ReMe/reme2/schema/file_edge.py
huangsen 8465f6d06e ```
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.
```
2026-05-11 19:45:53 +08:00

177 lines
6.4 KiB
Python

"""FileEdge — typed wikilink edge between vault files.
This module is the single source of truth for both the edge **schema**
and the **inline parser** that recovers edges from body text. Edges
live exclusively in body text (frontmatter is not walked); the
predicate vocabulary is **open** — any identifier-shaped token
(`[A-Za-z][A-Za-z0-9_]*`) that the parser sees is preserved verbatim
on `FileEdge.predicate`. Vocabulary curation, if any, is the
maintainer's job, not the schema's.
## Inline forms recognised by `parse_wikilinks`
[[X]] bare wikilink → predicate=None
extends:: [[X]] line-level Dataview → predicate="extends"
[extends:: [[X]]] inline-bracketed → predicate="extends"
extends:: [[A]], [[B]] multi-target → 2 edges
"""
from __future__ import annotations
import re
from pydantic import BaseModel, ConfigDict, Field
# -- Schema ---------------------------------------------------------------
class FileEdge(BaseModel):
"""5-field minimal edge model. No provenance, no confidence."""
model_config = ConfigDict(extra="forbid")
target: str = Field(..., description="Raw wikilink target as written in source.")
predicate: str | None = Field(
default=None,
description="Typed-edge predicate (Dataview-style). None for bare [[X]].",
)
anchor: str | None = Field(default=None, description="Heading or block anchor (after #).")
alias: str | None = Field(default=None, description="Display alias (after |).")
embed: bool = Field(default=False, description="True for `![[X]]` embed prefix.")
# -- Regexes --------------------------------------------------------------
# Bare wikilink. `(?:!)?` is non-capturing so `m.group(1)` stays the
# target — read the embed prefix off `m.group(0).startswith("!")`.
WIKILINK_RE = re.compile(
r"""
(?:!)?
\[\[
(?P<target>[^\]\|\#\n]+?)
(?:\#(?P<anchor>[^\]\|\n]+))?
(?:\|(?P<alias>[^\]\n]+))?
\]\]
""",
re.VERBOSE,
)
# Line-level Dataview field. Anchored MULTILINE; allows leading bullet
# (`-`/`*`/`+`) so `- extends:: [[X]]` works inside Markdown lists.
# Predicate identifier follows Dataview convention: letter, then
# letters / digits / underscore.
DATAVIEW_LINE_RE = re.compile(
r"^[ \t]*(?:[-*+][ \t]+)?(?P<predicate>[A-Za-z][A-Za-z0-9_]*)\s*::\s*(?P<value>.+?)\s*$",
re.MULTILINE,
)
# Inline-bracketed field: opens with `[predicate::`. The closing `]` is
# located by `_iter_inline_fields` via depth-counted bracket scan because
# the value may contain `[[wikilink]]` whose inner `[[…]]` brackets are
# part of the value, not field delimiters.
_INLINE_FIELD_OPEN_RE = re.compile(r"\[(?P<predicate>[A-Za-z][A-Za-z0-9_]*)\s*::\s*")
# -- Internal helpers -----------------------------------------------------
def _edge_from_wm(wm: re.Match, *, predicate: str | None) -> FileEdge:
anchor = wm.group("anchor")
alias = wm.group("alias")
return FileEdge(
target=wm.group("target").strip(),
anchor=anchor.strip() if anchor else None,
alias=alias.strip() if alias else None,
embed=wm.group(0).startswith("!"),
predicate=predicate,
)
def _iter_inline_fields(text: str) -> list[tuple[int, int, str, int]]:
"""Find inline-bracketed `[predicate:: …]` fields by depth scan.
Returns ``(start, end, predicate, value_start)`` tuples — `value_start`
is the absolute offset where the value begins inside `text`, used to
project wikilink spans back to absolute positions for dedup.
Newlines terminate the scan: an inline field that spans a line break
is treated as malformed and skipped (matches Dataview's parser).
"""
out: list[tuple[int, int, str, int]] = []
for m in _INLINE_FIELD_OPEN_RE.finditer(text):
value_start = m.end()
depth = 1 # the outer '[' was the regex's first character
i = value_start
n = len(text)
while i < n:
c = text[i]
if c == "\n":
break
if c == "[":
depth += 1
elif c == "]":
depth -= 1
if depth == 0:
out.append((m.start(), i + 1, m.group("predicate"), value_start))
break
i += 1
return out
# -- Public parsing API ---------------------------------------------------
def extract_wikilinks(text: str) -> list[str]:
"""Targets-only list of wikilinks in body text (no dedup).
Used by callers that need to follow links structurally without caring
about predicates (e.g. the ingestor's auto-discovery hint).
"""
if not text:
return []
return [m.group("target").strip() for m in WIKILINK_RE.finditer(text)]
def parse_wikilinks(text: str) -> list[FileEdge]:
"""Structured parse of all three edge forms.
Order is by source position. Wikilinks attributed to an inline-
bracketed or line-level field are not also reported as bare; the
same wikilink span is consumed exactly once.
"""
if not text:
return []
consumed: list[tuple[int, int]] = []
items: list[tuple[int, FileEdge]] = []
# 1. Inline-bracketed: most specific, handled first.
for field_start, field_end, predicate, value_start in _iter_inline_fields(text):
value = text[value_start : field_end - 1]
for wm in WIKILINK_RE.finditer(value):
items.append((field_start, _edge_from_wm(wm, predicate=predicate)))
consumed.append((field_start, field_end))
# 2. Line-level: skip if entirely inside an inline-bracketed span.
for m in DATAVIEW_LINE_RE.finditer(text):
if any(cs <= m.start() and m.end() <= ce for cs, ce in consumed):
continue
predicate = m.group("predicate")
value = m.group("value")
value_start = m.start("value")
for wm in WIKILINK_RE.finditer(value):
wl_start = value_start + wm.start()
wl_end = value_start + wm.end()
items.append((wl_start, _edge_from_wm(wm, predicate=predicate)))
consumed.append((wl_start, wl_end))
# 3. Bare: anything left over.
for wm in WIKILINK_RE.finditer(text):
s, e = wm.span()
if any(cs <= s and e <= ce for cs, ce in consumed):
continue
items.append((s, _edge_from_wm(wm, predicate=None)))
items.sort(key=lambda pair: pair[0])
return [edge for _, edge in items]