1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Add semantic flow and convergence web views

This commit is contained in:
Andraxion 2026-07-25 17:34:58 -04:00
parent f9f7105983
commit 6609edc804
14 changed files with 616 additions and 128 deletions

View file

@ -131,7 +131,7 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
visualization = results[11].structuredContent["visualization"]
self.assertTrue(visualization["read_only"])
self.assertTrue(visualization["project_bound"])
self.assertEqual("graph-browser@13", visualization["template"])
self.assertEqual("graph-browser@14", visualization["template"])
self.assertEqual("managed_idle", visualization["lifetime"]["policy"])
self.assertEqual("docforge_stop_visualization", visualization["lifetime"]["stop_tool"])
self.assertTrue(visualization["url"].startswith("http://127.0.0.1:"))

View file

@ -73,6 +73,8 @@ class VisualizationTests(unittest.TestCase):
second = snapshot.node("guide.workflow", depth=2, limit=2)
filtered = snapshot.filter_nodes(category="tag", value="canonical", limit=2)
source = snapshot.source("guide.workflow")
flow = snapshot.lineage("guide.workflow", limit=20)
web = snapshot.web("guide.workflow", depth=2, limit=20)
self.assertEqual(3, overview["node_count"])
self.assertEqual(2, overview["edge_count"])
@ -96,12 +98,128 @@ class VisualizationTests(unittest.TestCase):
"guide.workflow",
{node["node_id"] for node in first["nodes"]},
)
self.assertIn(
{
"source_id": "guide.foundation",
"relation": "depends_on",
"target_id": "guide.workflow",
"stored_source_id": "guide.workflow",
"stored_target_id": "guide.foundation",
"reversed": True,
},
flow["edges"],
)
self.assertEqual(flow["edges"], web["edges"])
self.assertEqual(
{"guide.foundation": 1, "guide.workflow": 0, "proof.validation": 1},
web["hops"],
)
with self.assertRaisesRegex(DocForgeError, "safety boundary"):
snapshot.node("guide.workflow", depth=1, limit=401)
with self.assertRaisesRegex(DocForgeError, "category is unsupported"):
snapshot.filter_nodes(category="relation", value="depends_on", limit=2)
def test_flow_reverses_imports_into_a_complete_structural_path(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
descriptor = root / ".docforge" / "project.toml"
descriptor.write_text(
descriptor.read_text(encoding="utf-8").replace(
'"depends_on", "proves",',
'"depends_on", "proves", "imports", "contains",',
),
encoding="utf-8",
)
workflow = root / "docs" / "content" / "workflow.md"
workflow.write_text(
workflow.read_text(encoding="utf-8").replace(
'depends_on = ["guide.foundation"]',
'depends_on = ["guide.foundation"]\n'
'imports = ["guide.foundation"]\n'
'contains = ["tests.example.example-tests.test-default"]',
),
encoding="utf-8",
)
(root / "docs" / "content" / "test-method.md").write_text(
"""+++
schema_version = 1
id = "tests.example.example-tests.test-default"
title = "Default behavior test"
family = "proof"
authority = "derived"
status = "approved"
tags = ["test"]
summary = "Exercises the default behavior."
+++
The test verifies the default behavior.
""",
encoding="utf-8",
)
index = ProjectIndex(Project.open(root))
index.build()
snapshot = VisualizationIndexSnapshot(index, index.check())
flow = snapshot.lineage(
"tests.example.example-tests.test-default",
limit=20,
)
edge_keys = {
(
edge["source_id"],
edge["relation"],
edge["target_id"],
edge["reversed"],
)
for edge in flow["edges"]
}
self.assertIn(
(
"guide.foundation",
"imports",
"guide.workflow",
True,
),
edge_keys,
)
self.assertIn(
(
"guide.workflow",
"contains",
"tests.example.example-tests.test-default",
False,
),
edge_keys,
)
self.assertEqual(
0,
flow["hops"]["tests.example.example-tests.test-default"],
)
self.assertEqual(1, flow["hops"]["guide.workflow"])
self.assertEqual(2, flow["hops"]["guide.foundation"])
web = snapshot.web("guide.workflow", depth=2, limit=20)
web_edges = {
(
edge["source_id"],
edge["relation"],
edge["target_id"],
edge["reversed"],
)
for edge in web["edges"]
}
self.assertIn(
(
"tests.example.example-tests.test-default",
"contains",
"guide.workflow",
True,
),
web_edges,
)
@unittest.skipUnless(shutil.which("node"), "Node.js is required for browser script validation")
def test_browser_javascript_is_valid(self) -> None:
with tempfile.TemporaryDirectory() as directory:
@ -118,6 +236,7 @@ class VisualizationTests(unittest.TestCase):
def test_browser_contains_hiding_source_navigation_and_scrollable_inspector(self) -> None:
self.assertIn('id="restore-hidden"', _GRAPH_BROWSER_HTML)
self.assertIn('id="view-web"', _GRAPH_BROWSER_HTML)
self.assertIn('id="open-node-source"', _GRAPH_BROWSER_HTML)
self.assertIn('id="hide-node"', _GRAPH_BROWSER_HTML)
self.assertIn('id="source-dialog"', _GRAPH_BROWSER_HTML)
@ -225,6 +344,31 @@ const containmentEdge = flow.edges.find((edge) => edge.relation === "contains"
if (!containmentEdge || containmentEdge.target_id !== "primary") {
fail("containment ancestry direction");
}
const webData = {
root: "focus",
hops: {focus: 0, downstream: 1, hidden: 2, ancestor: 3, alternate: 1},
nodes: [
{node_id: "focus"},
{node_id: "downstream"},
{node_id: "hidden"},
{node_id: "ancestor"},
{node_id: "alternate"},
],
edges: [
{source_id: "downstream", relation: "contains", target_id: "focus"},
{source_id: "hidden", relation: "contains", target_id: "downstream"},
{source_id: "ancestor", relation: "contains", target_id: "hidden"},
{source_id: "alternate", relation: "verifies", target_id: "focus"},
],
};
const web = buildWebGraph(webData);
const pruned = pruneConvergenceGraph(web, new Set(["hidden"]));
const prunedIds = new Set(pruned.nodes.map((node) => node.node_id));
if (prunedIds.has("hidden") || prunedIds.has("ancestor")) fail("hidden upstream pruning");
if (!prunedIds.has("downstream") || !prunedIds.has("alternate") || !prunedIds.has("focus")) {
fail("downstream convergence preservation");
}
if (pruned.prunedCount !== 2) fail("pruned node count");
"""
)
result = subprocess.run(
@ -292,6 +436,7 @@ if (!containmentEdge || containmentEdge.target_id !== "primary") {
self.assertIn('id="right-resizer"', html)
self.assertIn('id="view-nodes"', html)
self.assertIn('id="view-flow"', html)
self.assertIn('id="view-web"', html)
self.assertIn('id="neighborhood-sections"', html)
self.assertIn('id="relationship-key"', html)
self.assertIn('id="relationship-key-list"', html)
@ -322,11 +467,10 @@ if (!containmentEdge || containmentEdge.target_id !== "primary") {
self.assertIn("appendRelationMarker", javascript)
self.assertIn("renderRelationshipKey", javascript)
self.assertIn("buildFlowGraph", javascript)
self.assertIn("buildWebGraph", javascript)
self.assertIn("pruneConvergenceGraph", javascript)
self.assertIn("layoutFlow", javascript)
self.assertIn(
'api(`${showingFlow ? "lineage" : "node"}?${params}`)',
javascript,
)
self.assertIn('const endpoint = showingFlow ? "lineage"', javascript)
self.assertIn("filterByDescriptor", javascript)
self.assertIn("api(`filter?${params}`)", javascript)
self.assertIn('setViewMode("flow")', javascript)
@ -405,15 +549,25 @@ if (!containmentEdge || containmentEdge.target_id !== "primary") {
lineage = json.load(response)
self.assertTrue(lineage["lineage"])
self.assertEqual("guide.workflow", lineage["root"])
self.assertIn(
{
"source_id": "proof.validation",
"relation": "proves",
"target_id": "guide.workflow",
},
lineage["edges"],
self.assertTrue(
any(
edge["source_id"] == "proof.validation"
and edge["relation"] == "proves"
and edge["target_id"] == "guide.workflow"
and edge["reversed"] is False
for edge in lineage["edges"]
)
)
web_query = urllib.parse.urlencode(
{"id": "guide.workflow", "depth": 2, "limit": 1000}
)
with urllib.request.urlopen(f"{base}api/web?{web_query}", timeout=2) as response:
web = json.load(response)
self.assertTrue(web["web"])
self.assertEqual("guide.workflow", web["root"])
self.assertEqual(0, web["hops"]["guide.workflow"])
wrong_token = f"{first_url.scheme}://{first_url.netloc}/wrong-token/api/overview"
with self.assertRaises(urllib.error.HTTPError) as missing:
urllib.request.urlopen(wrong_token, timeout=2)