feat(okf): in-import cross-link graph — extract/resolve/reject links, dangling-link signal (T5a/A, TDD, +10)
This commit is contained in:
parent
320a40244f
commit
30aa0a42a1
2 changed files with 191 additions and 0 deletions
|
|
@ -26,11 +26,15 @@ from llm_ingestion_guard.okf import (
|
|||
trust_for,
|
||||
format_log_entry,
|
||||
import_bundle,
|
||||
extract_link_targets,
|
||||
resolve_link,
|
||||
link_graph,
|
||||
Origin,
|
||||
Channel,
|
||||
OKFFrontmatterError,
|
||||
OKFPathError,
|
||||
OKFResourceError,
|
||||
OKFLinkError,
|
||||
)
|
||||
from llm_ingestion_guard.report import Report
|
||||
from llm_ingestion_guard.disposition import Trust, Disposition
|
||||
|
|
@ -378,3 +382,65 @@ def test_import_bundle_records_origin_channel_on_stamp():
|
|||
assert stamp.trust is Trust.TRUSTED
|
||||
assert stamp.origin is Origin.INTERNAL
|
||||
assert stamp.channel is Channel.MANUAL
|
||||
|
||||
|
||||
# --- T5a/A: cross-link extraction, target validation, in-import resolution ----
|
||||
# OKF links are markdown `.md` paths, bundle-absolute (`/x.md`, recommended) or
|
||||
# relative (`./x.md`); verified against SPEC.md. In-import graph only (A); the
|
||||
# cross-run persisted graph (B) is deferred to stream 2 (see docs/PLAN.md).
|
||||
|
||||
def test_extract_link_targets_pulls_markdown_destinations():
|
||||
body = "See [users](/tables/users.md) and [orders](./orders.md) for detail."
|
||||
assert extract_link_targets(body) == ["/tables/users.md", "./orders.md"]
|
||||
|
||||
|
||||
def test_resolve_link_bundle_absolute_to_concept_id():
|
||||
assert resolve_link("/tables/customers.md", "docs/intro") == "tables/customers"
|
||||
|
||||
|
||||
def test_resolve_link_relative_to_concept_id():
|
||||
assert resolve_link("./other.md", "tables/users") == "tables/other"
|
||||
|
||||
|
||||
def test_resolve_link_relative_parent_stays_in_bundle():
|
||||
assert resolve_link("../ops/runbook.md", "tables/users") == "ops/runbook"
|
||||
|
||||
|
||||
def test_resolve_link_external_https_is_not_a_concept_edge():
|
||||
assert resolve_link("https://example.com/page", "tables/users") is None
|
||||
|
||||
|
||||
def test_resolve_link_rejects_dangerous_scheme():
|
||||
with pytest.raises(OKFLinkError):
|
||||
resolve_link("javascript:alert(1)", "tables/users")
|
||||
|
||||
|
||||
def test_resolve_link_rejects_bundle_escape():
|
||||
with pytest.raises(OKFLinkError):
|
||||
resolve_link("../../etc/passwd.md", "tables/users")
|
||||
|
||||
|
||||
def test_link_graph_flags_dangling_link():
|
||||
# a/main links to a not-yet-existent b/target -> dormant-injection signal (§7.2)
|
||||
bundle = {
|
||||
"a/main.md": "---\ntype: t\n---\nSee [later](/b/target.md).\n",
|
||||
"a/other.md": "---\ntype: t\n---\nNothing linked.\n",
|
||||
}
|
||||
graph = link_graph(bundle)
|
||||
assert ("a/main", "b/target") in graph.dangling
|
||||
|
||||
|
||||
def test_link_graph_resolves_present_target():
|
||||
bundle = {
|
||||
"a/main.md": "---\ntype: t\n---\nSee [here](/b/target.md).\n",
|
||||
"b/target.md": "---\ntype: t\n---\nThe target concept.\n",
|
||||
}
|
||||
graph = link_graph(bundle)
|
||||
assert ("a/main", "b/target") in graph.resolved
|
||||
assert graph.dangling == ()
|
||||
|
||||
|
||||
def test_link_graph_records_rejected_dangerous_link():
|
||||
bundle = {"a/main.md": "---\ntype: t\n---\n[x](javascript:alert(1))\n"}
|
||||
graph = link_graph(bundle)
|
||||
assert any(from_id == "a/main" for from_id, _target, _reason in graph.rejected)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue