mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-22 00:32:49 +00:00
docs(protocol): add typed edge link protocol documentation Add comprehensive documentation for the link protocol supporting typed edges in body text. This includes specification for three legal inline forms (bare wikilink, line-level Dataview, inline-bracketed Dataview), predicate syntax rules, and the machine-managed Relations section convention for organizing discovered edges. fix(memory): update path reference from vault_root to working_dir Change the memory_create operation's path anchoring from vault_root to working_dir to maintain consistency with the current working directory configuration. refactor(components): remove edge_extractor module and simplify parsing Remove the edge_extractor component module entirely and inline edge extraction logic directly into LinkedFileParser using parse_wikilinks utility. This simplifies the architecture by eliminating the separate edge extraction component and delegating edge discovery to the maintainer's enrichment operations. feat(parser): update parse method signature and simplify edge extraction Modify LinkedFileParser to return (FileNode, list[FileChunk]) tuple instead of ParsedFile, remove dependency on BaseEdgeExtractor, and implement direct wikilink parsing from body text only. ```
219 lines
7.1 KiB
Python
219 lines
7.1 KiB
Python
"""Wikilink protocol unit tests — body-only edge extraction.
|
|
|
|
Covers the three legal inline forms (bare / line-level Dataview /
|
|
inline-bracketed Dataview), multi-target expansion, dedup against
|
|
typed wrappers, open-vocabulary predicate pass-through, and the
|
|
explicit decision that frontmatter is no longer walked for links.
|
|
"""
|
|
|
|
from reme2.schema.file_edge import FileEdge, extract_wikilinks, parse_wikilinks
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Bare wikilinks
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_bare_wikilink():
|
|
edges = parse_wikilinks("see [[X]]")
|
|
assert len(edges) == 1
|
|
assert edges[0].target == "X"
|
|
assert edges[0].predicate is None
|
|
assert edges[0].embed is False
|
|
|
|
|
|
def test_bare_with_anchor_alias_embed():
|
|
edges = parse_wikilinks("![[X#sec|Alias]]")
|
|
assert len(edges) == 1
|
|
assert edges[0].target == "X"
|
|
assert edges[0].anchor == "sec"
|
|
assert edges[0].alias == "Alias"
|
|
assert edges[0].embed is True
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Line-level Dataview
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_line_level_field():
|
|
edges = parse_wikilinks("extends:: [[Source Topic]]")
|
|
assert len(edges) == 1
|
|
assert edges[0].target == "Source Topic"
|
|
assert edges[0].predicate == "extends"
|
|
|
|
|
|
def test_line_level_multi_target():
|
|
edges = parse_wikilinks("concerns:: [[A]], [[B]], [[C]]")
|
|
assert [(e.target, e.predicate) for e in edges] == [
|
|
("A", "concerns"),
|
|
("B", "concerns"),
|
|
("C", "concerns"),
|
|
]
|
|
|
|
|
|
def test_line_level_with_bullet():
|
|
edges = parse_wikilinks("- extends:: [[X]]\n * concerns:: [[Y]]")
|
|
assert [(e.target, e.predicate) for e in edges] == [
|
|
("X", "extends"),
|
|
("Y", "concerns"),
|
|
]
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Inline-bracketed Dataview
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_inline_bracketed():
|
|
edges = parse_wikilinks("This [extends:: [[Y]]] something else.")
|
|
assert len(edges) == 1
|
|
assert edges[0].target == "Y"
|
|
assert edges[0].predicate == "extends"
|
|
|
|
|
|
def test_inline_bracketed_multi_target():
|
|
edges = parse_wikilinks("[concerns:: [[A]], [[B]]]")
|
|
assert [(e.target, e.predicate) for e in edges] == [
|
|
("A", "concerns"),
|
|
("B", "concerns"),
|
|
]
|
|
|
|
|
|
def test_inline_bracketed_skips_cross_line():
|
|
# A `[predicate:: ...]` that spans a newline is malformed → the inner
|
|
# wikilink falls back to bare; the unmatched `[` does not eat tail text.
|
|
edges = parse_wikilinks("[extends:: [[X]]\nbad]")
|
|
assert len(edges) == 1
|
|
assert edges[0].target == "X"
|
|
assert edges[0].predicate is None
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Dedup: a wikilink inside a typed wrapper should not double-emit
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_inline_bracketed_does_not_double_emit():
|
|
edges = parse_wikilinks("see [extends:: [[X]]] again.")
|
|
assert len(edges) == 1
|
|
assert edges[0].predicate == "extends"
|
|
|
|
|
|
def test_line_level_value_does_not_double_emit():
|
|
edges = parse_wikilinks("extends:: [[X]]")
|
|
assert len(edges) == 1
|
|
|
|
|
|
def test_typed_and_bare_coexist_for_same_target():
|
|
edges = parse_wikilinks("extends:: [[X]]\nFree text mentioning [[X]] again.")
|
|
targets_preds = sorted(((e.target, e.predicate or "") for e in edges))
|
|
assert targets_preds == [("X", ""), ("X", "extends")]
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Open-vocabulary predicates — any identifier-shaped token passes through
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_arbitrary_predicate_preserved():
|
|
edges = parse_wikilinks("anything_goes:: [[X]]")
|
|
assert len(edges) == 1
|
|
assert edges[0].target == "X"
|
|
assert edges[0].predicate == "anything_goes"
|
|
|
|
|
|
def test_inline_arbitrary_predicate_preserved():
|
|
edges = parse_wikilinks("[wat:: [[X]]]")
|
|
assert len(edges) == 1
|
|
assert edges[0].predicate == "wat"
|
|
|
|
|
|
def test_predicate_must_be_identifier_shaped():
|
|
# A leading digit fails the regex `[A-Za-z][A-Za-z0-9_]*` so the line is
|
|
# not recognised as a Dataview field — the wikilink falls back to bare.
|
|
edges = parse_wikilinks("123bad:: [[X]]")
|
|
assert len(edges) == 1
|
|
assert edges[0].target == "X"
|
|
assert edges[0].predicate is None
|
|
|
|
|
|
def test_file_edge_accepts_any_predicate_string():
|
|
e = FileEdge(target="X", predicate="totally_made_up")
|
|
assert e.predicate == "totally_made_up"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Frontmatter is NOT walked for links — explicit regression
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_frontmatter_links_block_is_ignored():
|
|
# Even if a YAML-shaped string sits at the top of body, parse_wikilinks
|
|
# only operates on body. We pass body text directly here, so this test
|
|
# asserts the API surface no longer accepts a metadata dict.
|
|
import inspect
|
|
|
|
sig = inspect.signature(parse_wikilinks)
|
|
assert list(sig.parameters.keys()) == ["text"], (
|
|
"parse_wikilinks should accept body text only — frontmatter walk removed"
|
|
)
|
|
|
|
|
|
def test_no_frontmatter_walker_exported():
|
|
from reme2.schema import file_edge as wl
|
|
|
|
for removed in (
|
|
"parse_wikilinks_from_metadata",
|
|
"extract_wikilinks_from_metadata",
|
|
"extract_inline_fields",
|
|
"extract_typed_edges",
|
|
"InlineField",
|
|
):
|
|
assert not hasattr(wl, removed), (
|
|
f"{removed} should have been removed when YAML edges were dropped"
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# FileEdge schema
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_file_edge_extra_forbid():
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
with pytest.raises(ValidationError):
|
|
FileEdge(target="X", source="regex") # type: ignore[call-arg]
|
|
|
|
|
|
def test_file_edge_minimal_field_set():
|
|
e = FileEdge(target="X")
|
|
dumped = e.model_dump()
|
|
assert set(dumped.keys()) == {"target", "predicate", "anchor", "alias", "embed"}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Back-compat: extract_wikilinks returns flat target list (used by ingestor)
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_extract_wikilinks_flat_targets():
|
|
targets = extract_wikilinks("see [[X]] and extends:: [[Y]] and [extends:: [[Z]]]")
|
|
assert targets == ["X", "Y", "Z"]
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Source ordering stability
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_edges_sorted_by_source_position():
|
|
body = (
|
|
"intro [[First]] then\n"
|
|
"extends:: [[Second]]\n"
|
|
"tail [[Third]]\n"
|
|
)
|
|
edges = parse_wikilinks(body)
|
|
assert [e.target for e in edges] == ["First", "Second", "Third"]
|