Add function-scoped Logic visualization
This commit is contained in:
parent
9fcafc290c
commit
9b4258c852
22 changed files with 1420 additions and 62 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
126
tests/test_python_logic.py
Normal 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()
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue