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

Add function-scoped Logic visualization

This commit is contained in:
Andraxion 2026-07-25 21:08:43 -04:00
parent 9fcafc290c
commit 9b4258c852
22 changed files with 1420 additions and 62 deletions

View file

@ -41,6 +41,7 @@ from docforge.models import (
RenderConfig,
RenderView,
)
from docforge.visualization import VisualizationIndexSnapshot
class Loader:
@ -302,6 +303,16 @@ class AdapterContractTests(unittest.TestCase):
first = index.build()
self.assertEqual(2, first["build"]["reparsed_sources"])
self.assertEqual(0, first["build"]["cache_hits"])
self.assertEqual(1, first["logic_projection_count"])
logic = index.get_logic("guide.workflow")
self.assertTrue(logic["available"])
self.assertEqual("guide.workflow", logic["projection"]["owner_node_id"])
self.assertEqual(2, len(logic["projection"]["nodes"]))
self.assertFalse(index.get_logic("guide.foundation")["available"])
visual_logic = VisualizationIndexSnapshot(index, index.check()).logic("guide.workflow")
self.assertTrue(visual_logic["available"])
self.assertEqual("entry", visual_logic["root"])
self.assertEqual("return", visual_logic["edges"][0]["relation"])
loader.extract_calls.clear()
cache_path = root / ".cache" / "incremental" / "extractions.json"
cache_modified = cache_path.stat().st_mtime_ns

View file

@ -86,6 +86,7 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
("docforge_project_info", {}),
("docforge_get_contract", {}),
("docforge_get_node", {"node_id": "guide.workflow"}),
("docforge_get_logic", {"owner_node_id": "guide.workflow"}),
("docforge_search", {"query": "canonical nodes", "limit": 5}),
("docforge_filter_nodes", {"family": "proof", "tag": "validation"}),
("docforge_backlinks", {"node_id": "guide.workflow"}),
@ -126,18 +127,19 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
self.assertIn("arbitrary_renderer_execution", contract["excluded_operations"])
self.assertFalse(contract["isolated_changeset_writes_allowed"])
self.assertFalse(contract["proposal_access"]["enabled"])
self.assertTrue(results[10].structuredContent["configured"])
self.assertEqual("stale", results[10].structuredContent["state"])
visualization = results[11].structuredContent["visualization"]
self.assertFalse(results[3].structuredContent["available"])
self.assertTrue(results[11].structuredContent["configured"])
self.assertEqual("stale", results[11].structuredContent["state"])
visualization = results[12].structuredContent["visualization"]
self.assertTrue(visualization["read_only"])
self.assertTrue(visualization["project_bound"])
self.assertEqual("graph-browser@15", visualization["template"])
self.assertEqual("graph-browser@16", 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:"))
self.assertEqual("stopped", results[12].structuredContent["state"])
self.assertEqual("not_running", results[13].structuredContent["state"])
context = results[8].structuredContent
self.assertEqual("stopped", results[13].structuredContent["state"])
self.assertEqual("not_running", results[14].structuredContent["state"])
context = results[9].structuredContent
self.assertLessEqual(context["estimated_tokens"], 180)
self.assertTrue(context["omissions"])

126
tests/test_python_logic.py Normal file
View file

@ -0,0 +1,126 @@
from __future__ import annotations
import unittest
from docforge.python_logic import PythonLogicOwner, analyze_python_source
class PythonLogicTests(unittest.TestCase):
def projection(self, source: str, qualified_name: str, line: int = 1):
return analyze_python_source(
source,
source_id="source.example",
owners=(PythonLogicOwner("py.symbol.example", qualified_name, line),),
filename="example.py",
)[0]
def test_boolean_short_circuit_branches_and_terminals(self) -> None:
projection = self.projection(
"""\
def decide(enabled, cached, stale):
if enabled and (cached is None or stale):
return "fetch"
raise RuntimeError("disabled")
""",
"decide",
)
kinds = [node.kind for node in projection.nodes]
labels = [node.label for node in projection.nodes]
edge_labels = [edge.label for edge in projection.edges]
self.assertEqual(1, kinds.count("entry"))
self.assertEqual(1, kinds.count("exit"))
self.assertEqual(3, kinds.count("condition"))
self.assertIn("enabled", labels)
self.assertIn("cached is None", labels)
self.assertIn("stale", labels)
self.assertIn("TRUE", edge_labels)
self.assertIn("FALSE", edge_labels)
self.assertIn("RETURN", edge_labels)
self.assertIn("RAISE", edge_labels)
def test_loops_match_try_and_control_transfers_are_explicit(self) -> None:
projection = self.projection(
"""\
def process(items, mode):
for item in items:
if item.skip:
continue
if item.stop:
break
consume(item)
else:
finish()
match mode:
case "safe":
value = safe()
case _:
value = fallback()
try:
return value
except ValueError:
raise
finally:
cleanup()
""",
"process",
)
kinds = {node.kind for node in projection.nodes}
relations = {edge.relation for edge in projection.edges}
labels = {edge.label for edge in projection.edges}
self.assertTrue(
{"loop", "continue", "break", "case", "try", "except", "finally"}.issubset(kinds)
)
self.assertTrue(
{"loop", "continue", "break", "case", "exception", "return", "raise"}.issubset(
relations
)
)
self.assertIn("EXHAUSTED", labels)
self.assertIn("NEXT ITEM", labels)
self.assertIn("NEXT CASE", labels)
def test_class_methods_nested_functions_and_async_functions_use_explicit_owners(self) -> None:
source = """\
class Worker:
async def run(self):
async with self.session():
await self.step()
if self.enabled:
def nested():
return True
return nested()
"""
projections = analyze_python_source(
source,
source_id="source.worker",
owners=(
PythonLogicOwner("py.symbol.worker.run", "Worker.run", 2),
PythonLogicOwner("py.symbol.worker.nested", "Worker.run.nested", 7),
),
filename="worker.py",
)
self.assertEqual(
("py.symbol.worker.nested", "py.symbol.worker.run"),
tuple(projection.owner_node_id for projection in projections),
)
run = next(item for item in projections if item.owner_node_id.endswith(".run"))
self.assertIn("action", {node.kind for node in run.nodes})
self.assertIn("call", {node.kind for node in run.nodes})
def test_projection_is_deterministic(self) -> None:
source = """\
def choose(first, second):
return first if first else second
"""
first = self.projection(source, "choose")
second = self.projection(source, "choose")
self.assertEqual(first, second)
if __name__ == "__main__":
unittest.main()

View file

@ -237,6 +237,7 @@ The test verifies the default behavior.
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="view-logic"', _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)
@ -455,6 +456,7 @@ if (pruned.prunedCount !== 2) fail("pruned node count");
self.assertIn('id="view-nodes"', html)
self.assertIn('id="view-flow"', html)
self.assertIn('id="view-web"', html)
self.assertIn('id="view-logic"', html)
self.assertIn('id="neighborhood-sections"', html)
self.assertIn('id="relationship-key"', html)
self.assertIn('id="relationship-key-list"', html)
@ -595,6 +597,15 @@ if (pruned.prunedCount !== 2) fail("pruned node count");
self.assertEqual("guide.workflow", web["root"])
self.assertEqual(0, web["hops"]["guide.workflow"])
logic_query = urllib.parse.urlencode({"id": "guide.workflow"})
with urllib.request.urlopen(
f"{base}api/logic?{logic_query}", timeout=2
) as response:
logic = json.load(response)
self.assertTrue(logic["logic"])
self.assertFalse(logic["available"])
self.assertEqual("guide.workflow", logic["root"])
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)