Add multi-language logic exploration
This commit is contained in:
parent
9b4258c852
commit
9161889492
18 changed files with 1639 additions and 76 deletions
|
|
@ -133,7 +133,7 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
|
|||
visualization = results[12].structuredContent["visualization"]
|
||||
self.assertTrue(visualization["read_only"])
|
||||
self.assertTrue(visualization["project_bound"])
|
||||
self.assertEqual("graph-browser@16", visualization["template"])
|
||||
self.assertEqual("graph-browser@17", 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:"))
|
||||
|
|
|
|||
154
tests/test_treesitter_logic.py
Normal file
154
tests/test_treesitter_logic.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from docforge.errors import DocForgeError
|
||||
from docforge.treesitter_logic import (
|
||||
TreeSitterLogicOwner,
|
||||
analyze_cpp_source,
|
||||
analyze_javascript_source,
|
||||
)
|
||||
|
||||
|
||||
class JavaScriptLogicTests(unittest.TestCase):
|
||||
def test_branches_short_circuit_and_converge(self) -> None:
|
||||
source = """
|
||||
function choose(enabled, ready) {
|
||||
if (enabled && ready()) {
|
||||
accept();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
""".strip()
|
||||
projection = analyze_javascript_source(
|
||||
source,
|
||||
source_id="source.javascript",
|
||||
owners=(TreeSitterLogicOwner("js.symbol.choose", "choose", 1),),
|
||||
)[0]
|
||||
|
||||
kinds = {node.kind for node in projection.nodes}
|
||||
labels = {node.label for node in projection.nodes}
|
||||
relations = {edge.relation for edge in projection.edges}
|
||||
self.assertIn("condition", kinds)
|
||||
self.assertIn("convergence", kinds)
|
||||
self.assertIn("Decision convergence", labels)
|
||||
self.assertIn("when_true", relations)
|
||||
self.assertIn("when_false", relations)
|
||||
self.assertIn("return", relations)
|
||||
|
||||
def test_arrow_function_expression_is_a_returning_projection(self) -> None:
|
||||
source = "const choose = (enabled) => enabled ? accept() : reject();"
|
||||
projection = analyze_javascript_source(
|
||||
source,
|
||||
source_id="source.javascript",
|
||||
owners=(TreeSitterLogicOwner("js.symbol.choose", "choose", 1),),
|
||||
)[0]
|
||||
|
||||
labels = {node.label for node in projection.nodes}
|
||||
self.assertIn("enabled", labels)
|
||||
self.assertIn("return accept()", labels)
|
||||
self.assertIn("return reject()", labels)
|
||||
|
||||
def test_loops_switch_and_exception_paths_are_preserved(self) -> None:
|
||||
source = """
|
||||
function process(items, mode) {
|
||||
for (const item of items) {
|
||||
if (!item.ready) continue;
|
||||
use(item);
|
||||
}
|
||||
switch (mode) {
|
||||
case 1:
|
||||
one();
|
||||
break;
|
||||
default:
|
||||
fallback();
|
||||
}
|
||||
try {
|
||||
risk();
|
||||
} catch (error) {
|
||||
recover(error);
|
||||
} finally {
|
||||
clean();
|
||||
}
|
||||
}
|
||||
""".strip()
|
||||
projection = analyze_javascript_source(
|
||||
source,
|
||||
source_id="source.javascript",
|
||||
owners=(TreeSitterLogicOwner("js.symbol.process", "process", 1),),
|
||||
)[0]
|
||||
|
||||
kinds = {node.kind for node in projection.nodes}
|
||||
labels = {node.label for node in projection.nodes}
|
||||
relations = {edge.relation for edge in projection.edges}
|
||||
self.assertTrue({"loop", "continue", "case", "try", "except", "finally"} <= kinds)
|
||||
self.assertIn("Case convergence", labels)
|
||||
self.assertIn("Exception convergence", labels)
|
||||
self.assertTrue({"loop", "continue", "case", "exception"} <= relations)
|
||||
|
||||
|
||||
class CppLogicTests(unittest.TestCase):
|
||||
def test_cpp_function_branches_and_throws(self) -> None:
|
||||
source = """
|
||||
int choose(bool enabled) {
|
||||
if (enabled) {
|
||||
return 1;
|
||||
}
|
||||
throw Error();
|
||||
}
|
||||
""".strip()
|
||||
projection = analyze_cpp_source(
|
||||
source,
|
||||
source_id="source.cpp",
|
||||
owners=(TreeSitterLogicOwner("cpp.symbol.choose", "choose", 1),),
|
||||
)[0]
|
||||
|
||||
kinds = {node.kind for node in projection.nodes}
|
||||
relations = {edge.relation for edge in projection.edges}
|
||||
self.assertTrue({"entry", "condition", "return", "raise", "exit"} <= kinds)
|
||||
self.assertTrue({"when_true", "when_false", "return", "raise"} <= relations)
|
||||
|
||||
def test_cpp_qualified_method_owner_is_resolved(self) -> None:
|
||||
source = """
|
||||
class Worker {
|
||||
public:
|
||||
int run(bool ready) {
|
||||
while (ready) {
|
||||
ready = tick();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
""".strip()
|
||||
projection = analyze_cpp_source(
|
||||
source,
|
||||
source_id="source.cpp",
|
||||
owners=(TreeSitterLogicOwner("cpp.symbol.worker.run", "Worker.run", 3),),
|
||||
)[0]
|
||||
|
||||
labels = {node.label for node in projection.nodes}
|
||||
self.assertIn("Loop exit", labels)
|
||||
self.assertIn("return 0", labels)
|
||||
|
||||
def test_invalid_source_and_missing_owner_fail_closed(self) -> None:
|
||||
with self.assertRaises(DocForgeError) as invalid:
|
||||
analyze_cpp_source(
|
||||
"int broken( {",
|
||||
source_id="source.cpp",
|
||||
owners=(),
|
||||
)
|
||||
self.assertEqual(invalid.exception.code, "invalid_logic_source")
|
||||
|
||||
with self.assertRaises(DocForgeError) as missing:
|
||||
analyze_javascript_source(
|
||||
"function exists() {}",
|
||||
source_id="source.javascript",
|
||||
owners=(TreeSitterLogicOwner("js.symbol.missing", "missing", 1),),
|
||||
)
|
||||
self.assertEqual(missing.exception.code, "missing_logic_owner")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -72,6 +72,14 @@ class VisualizationTests(unittest.TestCase):
|
|||
first = snapshot.node("guide.workflow", depth=2, limit=2)
|
||||
second = snapshot.node("guide.workflow", depth=2, limit=2)
|
||||
filtered = snapshot.filter_nodes(category="tag", value="canonical", limit=2)
|
||||
searched = snapshot.search(
|
||||
query="",
|
||||
family="guide",
|
||||
kind="canonical",
|
||||
language=None,
|
||||
capability="source",
|
||||
limit=2,
|
||||
)
|
||||
source = snapshot.source("guide.workflow")
|
||||
flow = snapshot.lineage("guide.workflow", limit=20)
|
||||
web = snapshot.web("guide.workflow", depth=2, limit=20)
|
||||
|
|
@ -91,6 +99,11 @@ class VisualizationTests(unittest.TestCase):
|
|||
self.assertEqual(1, filtered["total"])
|
||||
self.assertFalse(filtered["truncated"])
|
||||
self.assertEqual("guide.foundation", filtered["results"][0]["node_id"])
|
||||
self.assertEqual(1, searched["count"])
|
||||
self.assertEqual("guide.foundation", searched["results"][0]["node_id"])
|
||||
self.assertIn({"value": "canonical", "count": 1}, overview["tags"])
|
||||
self.assertIn({"value": "source", "count": 3}, overview["capabilities"])
|
||||
self.assertIn({"value": "logic", "count": 0}, overview["capabilities"])
|
||||
self.assertLessEqual(len(first["edges"]), 2)
|
||||
self.assertEqual("docs/content/workflow.md", source["source_path"])
|
||||
self.assertIn("Editors change canonical nodes", source["content"])
|
||||
|
|
@ -238,10 +251,17 @@ The test verifies the default behavior.
|
|||
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="kind"', _GRAPH_BROWSER_HTML)
|
||||
self.assertIn('id="language"', _GRAPH_BROWSER_HTML)
|
||||
self.assertIn('id="capability"', _GRAPH_BROWSER_HTML)
|
||||
self.assertIn('data-preset="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)
|
||||
self.assertIn("state.hiddenNodes.add(nodeId)", _GRAPH_BROWSER_JAVASCRIPT)
|
||||
self.assertIn("applyTraceHighlight", _GRAPH_BROWSER_JAVASCRIPT)
|
||||
self.assertIn("layoutLogic", _GRAPH_BROWSER_JAVASCRIPT)
|
||||
self.assertIn("trace-connected", _GRAPH_BROWSER_CSS)
|
||||
self.assertIn(
|
||||
"grid-template-rows: auto minmax(0, 1fr) auto",
|
||||
_GRAPH_BROWSER_CSS,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue