chaincompiler-si 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: chaincompiler-si
3
+ Version: 0.3.0
4
+ Summary: Self-Interpreter for the ChainCompiler chain dialect — an MCP that is also a skill
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: chaincompiler==0.3.0
7
+ Requires-Dist: chainaios==0.3.0
8
+ Requires-Dist: chaincompiler-accc==0.3.0
9
+ Requires-Dist: corcc==0.3.0
10
+ Requires-Dist: sccc==0.3.0
11
+ Requires-Dist: agent-skilltree>=0.4.0
12
+ Requires-Dist: fastmcp
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chaincompiler-si"
7
+ version = "0.3.0"
8
+ description = "Self-Interpreter for the ChainCompiler chain dialect — an MCP that is also a skill"
9
+ requires-python = ">=3.11"
10
+ dependencies = ["chaincompiler==0.3.0", "chainaios==0.3.0", "chaincompiler-accc==0.3.0", "corcc==0.3.0", "sccc==0.3.0", "agent-skilltree>=0.4.0", "fastmcp"]
11
+
12
+ [project.scripts]
13
+ si-server = "si.server:main"
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
17
+
18
+ [tool.setuptools.package-data]
19
+ si = ["skill/SKILL.md"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: chaincompiler-si
3
+ Version: 0.3.0
4
+ Summary: Self-Interpreter for the ChainCompiler chain dialect — an MCP that is also a skill
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: chaincompiler==0.3.0
7
+ Requires-Dist: chainaios==0.3.0
8
+ Requires-Dist: chaincompiler-accc==0.3.0
9
+ Requires-Dist: corcc==0.3.0
10
+ Requires-Dist: sccc==0.3.0
11
+ Requires-Dist: agent-skilltree>=0.4.0
12
+ Requires-Dist: fastmcp
@@ -0,0 +1,17 @@
1
+ pyproject.toml
2
+ src/chaincompiler_si.egg-info/PKG-INFO
3
+ src/chaincompiler_si.egg-info/SOURCES.txt
4
+ src/chaincompiler_si.egg-info/dependency_links.txt
5
+ src/chaincompiler_si.egg-info/entry_points.txt
6
+ src/chaincompiler_si.egg-info/requires.txt
7
+ src/chaincompiler_si.egg-info/top_level.txt
8
+ src/si/__init__.py
9
+ src/si/interp.py
10
+ src/si/mcp_tree.py
11
+ src/si/runtime.py
12
+ src/si/scaffold.py
13
+ src/si/server.py
14
+ src/si/skill/SKILL.md
15
+ tests/test_scaffold.py
16
+ tests/test_si.py
17
+ tests/test_si_regressions.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ si-server = si.server:main
@@ -0,0 +1,7 @@
1
+ chaincompiler==0.3.0
2
+ chainaios==0.3.0
3
+ chaincompiler-accc==0.3.0
4
+ corcc==0.3.0
5
+ sccc==0.3.0
6
+ agent-skilltree>=0.4.0
7
+ fastmcp
@@ -0,0 +1,20 @@
1
+ """si — the Self-Interpreter for the ChainCompiler chain dialect.
2
+
3
+ A Python dialect: the interpreter defers to native Python and only injects the
4
+ ChainCompiler vocabulary. Ships as an MCP server (si.server) that is ALSO a skill
5
+ (si/skill/SKILL.md), and can turn skill trees / exchange masters into MCPs.
6
+ The execute arm of Compilero Bandito; the self-hosting capstone.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ __version__ = "0.3.0"
11
+
12
+ from .interp import interpret, make_env
13
+ from .mcp_tree import build_tree_server, tree_to_mcp
14
+ from .runtime import reachable, read_skill, walk
15
+ from .scaffold import scaffold_repo, validate_node
16
+
17
+ __all__ = [
18
+ "interpret", "make_env", "walk", "reachable", "read_skill",
19
+ "build_tree_server", "tree_to_mcp", "scaffold_repo", "validate_node", "__version__",
20
+ ]
@@ -0,0 +1,58 @@
1
+ """The Self-Interpreter for the chain dialect.
2
+
3
+ The dialect is **a Python dialect** — so the interpreter does NOT re-wrap Python's
4
+ native ops (arithmetic, control flow, data). It runs the program with Python, and
5
+ only *injects the ChainCompiler vocabulary* as builtins: forge/gate/package the
6
+ chains, build/walk skill trees, build exchanges, turn trees into MCPs. A program
7
+ sets `result` (or the namespace is returned) — that's the value.
8
+
9
+ Self-interpreter: programs can themselves forge languages, build trees, and emit
10
+ MCPs — including ones that interpret further. ChainCompiler thereby runs itself.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+
17
+ def make_env(extra: dict[str, Any] | None = None) -> dict[str, Any]:
18
+ """The dialect's vocabulary — ChainCompiler primitives, injected as names."""
19
+ import accc
20
+ import chaincompiler
21
+ import chainaios
22
+ import corcc
23
+ import sccc
24
+ import skilltree
25
+
26
+ from . import runtime
27
+ from .mcp_tree import tree_to_mcp
28
+
29
+ env: dict[str, Any] = {
30
+ # construct (the AIOS top-door now lives in chainaios; write_skill stays in the CC base)
31
+ "construct_language": chainaios.construct_language,
32
+ "write_skill": chaincompiler.write_skill,
33
+ "forge_ac": accc.forge, "package_ac": accc.package, "gate_ac": accc.gate,
34
+ "forge_cor": corcc.forge_persona, "package_cor": corcc.package, "lint_cor": corcc.lint,
35
+ "forge_sc": sccc.forge, "package_sc": sccc.package, "lint_sc": sccc.lint,
36
+ # organize
37
+ "SkillTree": skilltree.SkillTree, "TreeNode": skilltree.TreeNode,
38
+ "materialize": skilltree.materialize, "validate_tree": skilltree.validate,
39
+ "build_exchange": skilltree.build_exchange, "load_exchange": skilltree.load_exchange,
40
+ # execute / search / emit
41
+ "walk": runtime.walk, "reachable": runtime.reachable, "read_skill": runtime.read_skill,
42
+ "tree_to_mcp": tree_to_mcp,
43
+ "result": None,
44
+ }
45
+ if extra:
46
+ env.update(extra)
47
+ return env
48
+
49
+
50
+ def interpret(source: str, *, env: dict[str, Any] | None = None) -> Any:
51
+ """Interpret a chain-dialect program. Returns `result` (or the eval of a single expr)."""
52
+ ns = make_env(env)
53
+ try:
54
+ code = compile(source, "<si>", "eval") # single expression?
55
+ return eval(code, ns)
56
+ except SyntaxError:
57
+ exec(compile(source, "<si>", "exec"), ns) # full program
58
+ return ns.get("result")
@@ -0,0 +1,57 @@
1
+ """Turn a skill tree (or exchange master) into an MCP server.
2
+
3
+ `build_tree_server` makes a FastMCP server that exposes a materialized tree's
4
+ skills as tools (list / read / walk). `tree_to_mcp` writes a tiny launcher so any
5
+ tree or master becomes a runnable MCP — that's "make trees and masters into MCPs".
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from . import runtime
12
+
13
+
14
+ def build_tree_server(root_dir: str | Path, root_name: str, *, name: str | None = None):
15
+ """A FastMCP server exposing the tree's skills. (Run with .run() for stdio.)"""
16
+ from fastmcp import FastMCP
17
+
18
+ root_dir = str(root_dir)
19
+ server = FastMCP(name or f"skilltree:{root_name}")
20
+
21
+ @server.tool()
22
+ def list_skills() -> list[str]:
23
+ """List every skill reachable from this tree's root."""
24
+ return runtime.reachable(root_dir, root_name)
25
+
26
+ @server.tool()
27
+ def read_skill(skill: str) -> str:
28
+ """Return the body of a skill in this tree by name."""
29
+ body = runtime.read_skill(root_dir, skill)
30
+ return body if body is not None else f"(no skill named {skill!r} in this tree)"
31
+
32
+ @server.tool()
33
+ def walk() -> dict:
34
+ """Return the full tree, traversed by following its breadcrumbs."""
35
+ return runtime.walk(root_dir, root_name)
36
+
37
+ return server
38
+
39
+
40
+ _LAUNCHER = '''#!/usr/bin/env python3
41
+ """Auto-generated MCP launcher for skill tree {name!r} (root {root!r})."""
42
+ from si.mcp_tree import build_tree_server
43
+
44
+ if __name__ == "__main__":
45
+ build_tree_server({root_dir!r}, {root!r}, name={name!r}).run()
46
+ '''
47
+
48
+
49
+ def tree_to_mcp(root_dir: str | Path, root_name: str, out_path: str | Path,
50
+ *, name: str | None = None) -> Path:
51
+ """Write a runnable MCP launcher that serves this tree/master. Returns its path."""
52
+ out = Path(out_path)
53
+ out.parent.mkdir(parents=True, exist_ok=True)
54
+ out.write_text(_LAUNCHER.format(root_dir=str(Path(root_dir).resolve()),
55
+ root=root_name, name=name or f"skilltree:{root_name}"),
56
+ encoding="utf-8")
57
+ return out
@@ -0,0 +1,62 @@
1
+ """The execute arm — walk a SkillTree by following its Read-breadcrumbs.
2
+
3
+ This is what "running" a tree means: start at the root SKILL.md, parse the
4
+ breadcrumb paths, and recurse. No new mechanism — the same breadcrumbs the
5
+ validator checks are the ones the runtime follows. (The breadcrumb verb is the
6
+ Read tool, not `cat`; the path is what we follow.)
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ import re
12
+
13
+ _CRUMB = re.compile(r"`([^`]+/SKILL\.md)`") # the backticked SKILL.md path, verb-agnostic
14
+ _COORD = re.compile(r"^\d+(?:\.\d+)*-") # a coordinate-addressed folder: `0.1-<name>`
15
+
16
+
17
+ def _skill_md(node_dir: Path, name: str) -> Path:
18
+ return Path(node_dir) / ".claude" / "skills" / name / "SKILL.md"
19
+
20
+
21
+ def _strip_front(text: str) -> str:
22
+ return text.split("---", 2)[-1].strip() if text.lstrip().startswith("---") else text.strip()
23
+
24
+
25
+ def _walk_md(md: Path) -> dict:
26
+ node = {"name": md.parent.name, "skill": str(md), "children": []}
27
+ if not md.is_file():
28
+ node["error"] = "missing"
29
+ return node
30
+ node_dir = md.parents[3] # <node>/.claude/skills/<name>/SKILL.md → <node>
31
+ for p in _CRUMB.findall(md.read_text(encoding="utf-8")):
32
+ child = Path(p)
33
+ if not child.is_absolute(): # a portable crumb is relative to the node's folder
34
+ child = node_dir / child # (SkillTree ≥0.4.0); a localized one is absolute
35
+ node["children"].append(_walk_md(child))
36
+ return node
37
+
38
+
39
+ def walk(root_dir: str | Path, root_name: str) -> dict:
40
+ """Traverse the tree from its root by following breadcrumbs. Returns a nested dict."""
41
+ return _walk_md(_skill_md(Path(root_dir), root_name))
42
+
43
+
44
+ def reachable(root_dir: str | Path, root_name: str) -> list[str]:
45
+ """Flat list of every skill name reachable from the root (the execute frontier)."""
46
+ out: list[str] = []
47
+ def rec(n):
48
+ out.append(n["name"])
49
+ for c in n["children"]:
50
+ rec(c)
51
+ rec(walk(root_dir, root_name))
52
+ return out
53
+
54
+
55
+ def read_skill(root_dir: str | Path, name: str) -> str | None:
56
+ """Search the tree for a skill by name; return its body (the search arm). A coordinate-addressed
57
+ tree names each skill's folder `<coord>-<name>`; the name matches with or without the coordinate."""
58
+ for md in Path(root_dir).rglob("SKILL.md"):
59
+ folder = md.parent.name
60
+ if folder == name or _COORD.sub("", folder) == name:
61
+ return _strip_front(md.read_text(encoding="utf-8"))
62
+ return None
@@ -0,0 +1,71 @@
1
+ """P5.1 — scaffold a federated node repo from a skill tree.
2
+
3
+ When the SI emits an MCP, it can also stamp a ChainCompiler-SHAPED node repo: the
4
+ skill tree (breadcrumbs), a marketplace registry that **federates under a
5
+ parent**, a runnable MCP launcher, and a README. The fractal made real — each
6
+ emitted MCP becomes a repo like ours, its marketplace joining under ours.
7
+
8
+ Full CI mechanics (deploy workflow, site generator) are P5.2; this stamps the
9
+ load-bearing shape: tree + registry + MCP + README, all validatable.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ from skilltree import SkillTree, Violation, materialize
17
+ from skilltree import validate as validate_tree
18
+
19
+ from .mcp_tree import tree_to_mcp
20
+
21
+ _NODE_README = """# {name} — a ChainCompiler node
22
+
23
+ A self-contained skill tree, served as an MCP, federated under `{parent}`.
24
+
25
+ - **Walk it** — start at `.claude/skills/{root}/SKILL.md` and Read each breadcrumb to descend.
26
+ - **Serve it** — `python serve_mcp.py` (an MCP exposing this tree's skills).
27
+ - **Marketplace** — see `registry.json`; this node registers under its parent.
28
+ """
29
+
30
+
31
+ def scaffold_repo(name: str, tree: SkillTree | str | Path, out_dir: str | Path,
32
+ *, parent: str | None = None) -> Path:
33
+ """Stamp a federated node repo at <out_dir>/<name>. Returns the repo path."""
34
+ if isinstance(tree, (str, Path)):
35
+ tree = SkillTree.load(tree)
36
+ repo = Path(out_dir) / name
37
+ repo.mkdir(parents=True, exist_ok=True)
38
+
39
+ materialize(tree, repo) # the node's skill tree (breadcrumbs)
40
+ root = tree.root.name
41
+
42
+ (repo / "registry.json").write_text(json.dumps({ # marketplace, federated
43
+ "name": name,
44
+ "parent": parent, # null at the root; else a repo ref
45
+ "entries": [{"name": root, "kind": "tree", "manifest": "skilltree.json",
46
+ "trust": "unverified"}],
47
+ }, indent=2), encoding="utf-8")
48
+
49
+ tree_to_mcp(repo, root, repo / "serve_mcp.py", name=f"node:{name}") # the runnable MCP
50
+ (repo / "README.md").write_text(
51
+ _NODE_README.format(name=name, parent=parent or "(root)", root=root), encoding="utf-8")
52
+ return repo
53
+
54
+
55
+ def validate_node(repo: str | Path) -> list[Violation]:
56
+ """Validate a scaffolded node: the tree + the registry + the MCP launcher."""
57
+ repo = Path(repo)
58
+ out = list(validate_tree(repo))
59
+ reg = repo / "registry.json"
60
+ if not reg.is_file():
61
+ out.append(Violation("error", repo.name, "missing registry.json"))
62
+ else:
63
+ try:
64
+ data = json.loads(reg.read_text(encoding="utf-8"))
65
+ if "parent" not in data or "entries" not in data:
66
+ out.append(Violation("error", repo.name, "registry.json missing parent/entries"))
67
+ except json.JSONDecodeError:
68
+ out.append(Violation("error", repo.name, "registry.json is not valid JSON"))
69
+ if not (repo / "serve_mcp.py").is_file():
70
+ out.append(Violation("error", repo.name, "missing serve_mcp.py launcher"))
71
+ return out
@@ -0,0 +1,77 @@
1
+ """The Self-Interpreter as an MCP server (and it is also a skill — see si/skill/).
2
+
3
+ Exposes the chain dialect to any MCP client:
4
+ - si_interpret(source) run a chain-dialect program
5
+ - si_walk(root_dir, root_name) traverse a materialized tree
6
+ - si_reachable(root_dir, root_name) list reachable skills
7
+ - si_read_skill(root_dir, skill) fetch a skill body by name
8
+ - si_tree_to_mcp(root_dir, name, out) turn a tree/master into its own MCP
9
+ - si_search(root_dir, query) BM25 over a tree, optionally coordinate-scoped
10
+
11
+ Run: python -m si.server (or the `si-server` console script)
12
+
13
+ The tool functions stay plain callables (registered, not rebound) so they're
14
+ usable directly and in tests, not only over the wire.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import re
19
+
20
+ from fastmcp import FastMCP
21
+
22
+ from . import runtime
23
+ from .interp import interpret
24
+ from .mcp_tree import tree_to_mcp
25
+
26
+ mcp = FastMCP("self-interpreter")
27
+
28
+
29
+ def si_interpret(source: str) -> str:
30
+ """Interpret a ChainCompiler chain-dialect program; returns repr(result)."""
31
+ return repr(interpret(source))
32
+
33
+
34
+ def si_walk(root_dir: str, root_name: str) -> dict:
35
+ """Traverse a materialized skill tree by following its breadcrumbs."""
36
+ return runtime.walk(root_dir, root_name)
37
+
38
+
39
+ def si_reachable(root_dir: str, root_name: str) -> list[str]:
40
+ """List every skill reachable from a tree's root."""
41
+ return runtime.reachable(root_dir, root_name)
42
+
43
+
44
+ def si_read_skill(root_dir: str, skill: str) -> str:
45
+ """Search a tree for a skill by name and return its body."""
46
+ body = runtime.read_skill(root_dir, skill)
47
+ return body if body is not None else f"(no skill named {skill!r})"
48
+
49
+
50
+ def si_tree_to_mcp(root_dir: str, root_name: str, out_path: str) -> str:
51
+ """Turn a skill tree (or exchange master) into a runnable MCP launcher."""
52
+ return str(tree_to_mcp(root_dir, root_name, out_path))
53
+
54
+
55
+ def si_search(root_dir: str, query: str, scope_coord: str | None = None, limit: int = 10) -> list[dict]:
56
+ """Search the skill tree (BM25), optionally scoped to a coordinate subtree (e.g. '0.1'). SkillTree's search hands a
57
+ query's words to FTS5 unquoted, so it raises on a query with no word and on an operator word (`NOT`, `AND`); this
58
+ door sends it the query's words alone, lowercased — FTS5's operators are capitals and its index folds case, so the
59
+ words match as before — and answers a query with no word with nothing."""
60
+ words = re.findall(r"\w+", query.lower())
61
+ if not words:
62
+ return []
63
+ from skilltree import search_tree
64
+ return search_tree(root_dir, " ".join(words), scope_coord=scope_coord, limit=limit)
65
+
66
+
67
+ # Register as MCP tools without rebinding the names (keep them plain callables).
68
+ for _fn in (si_interpret, si_walk, si_reachable, si_read_skill, si_tree_to_mcp, si_search):
69
+ mcp.tool()(_fn)
70
+
71
+
72
+ def main() -> None:
73
+ mcp.run()
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: self-interpreter
3
+ description: Run ChainCompiler chain-dialect programs and turn skill trees into MCPs. Use to forge languages, build/walk skill trees, or execute a cognition chain.
4
+ ---
5
+
6
+ # Self-Interpreter (SI)
7
+
8
+ The SI runs the **chain dialect** — a Python dialect whose vocabulary is the
9
+ ChainCompiler primitives (`forge_ac`, `forge_cor`, `forge_sc`, `construct_language`,
10
+ `SkillTree`, `TreeNode`, `materialize`, `walk`, `build_exchange`, `tree_to_mcp`, …).
11
+ It defers to native Python for everything else.
12
+
13
+ ## As an MCP
14
+
15
+ Start it: `python -m si.server`. Tools:
16
+
17
+ - `si_interpret(source)` — run a chain-dialect program; returns `repr(result)`.
18
+ - `si_walk(root_dir, root_name)` / `si_reachable(...)` — traverse a skill tree.
19
+ - `si_read_skill(root_dir, skill)` — fetch a skill body by name.
20
+ - `si_tree_to_mcp(root_dir, root_name, out)` — turn a tree/master into its own MCP.
21
+
22
+ ## Example program
23
+
24
+ ```python
25
+ tree = SkillTree(TreeNode("root", "sc", children=[TreeNode("a", "ac"), TreeNode("b", "cor")]))
26
+ materialize(tree, OUT)
27
+ result = reachable(OUT, "root") # the execute arm: walk the tree
28
+ ```
@@ -0,0 +1,40 @@
1
+ """P5.1 tests: scaffold a federated node repo and validate it."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from si import scaffold_repo, validate_node
8
+ from skilltree import SkillTree, TreeNode
9
+
10
+
11
+ def _tree() -> SkillTree:
12
+ return SkillTree(TreeNode("cc-skill-tree", "sc", children=[
13
+ TreeNode("reason", "cor", children=[TreeNode("simplify", "ac")]),
14
+ ]))
15
+
16
+
17
+ def test_scaffold_stamps_a_chaincompiler_shaped_node(tmp_path: Path):
18
+ repo = scaffold_repo("debug-node", _tree(), tmp_path, parent="github:sancovp/chaincompiler")
19
+ # the load-bearing shape: tree + registry + MCP launcher + README
20
+ assert (repo / ".claude" / "skills" / "cc-skill-tree" / "SKILL.md").is_file()
21
+ assert (repo / "skilltree.json").is_file()
22
+ assert (repo / "registry.json").is_file()
23
+ assert (repo / "serve_mcp.py").is_file()
24
+ assert (repo / "README.md").is_file()
25
+ assert validate_node(repo) == []
26
+
27
+
28
+ def test_registry_federates_under_parent(tmp_path: Path):
29
+ repo = scaffold_repo("node-x", _tree(), tmp_path, parent="github:sancovp/chaincompiler")
30
+ reg = json.loads((repo / "registry.json").read_text())
31
+ assert reg["parent"] == "github:sancovp/chaincompiler" # joins under ours
32
+ assert reg["entries"][0]["name"] == "cc-skill-tree"
33
+ assert reg["entries"][0]["trust"] == "unverified" # untrusted by default
34
+
35
+
36
+ def test_validate_node_catches_a_broken_node(tmp_path: Path):
37
+ repo = scaffold_repo("node-y", _tree(), tmp_path)
38
+ (repo / "serve_mcp.py").unlink() # remove the MCP launcher
39
+ issues = [v for v in validate_node(repo) if v.severity == "error"]
40
+ assert any("serve_mcp.py" in v.message for v in issues)
@@ -0,0 +1,85 @@
1
+ """SI tests: the dialect interpreter, the walk runtime, and tree→MCP."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from si import build_tree_server, interpret, reachable, read_skill, tree_to_mcp
7
+
8
+
9
+ def test_interpret_single_expression():
10
+ assert interpret("2 + 3") == 5 # native Python op, not re-wrapped
11
+
12
+
13
+ def test_interpret_builds_and_walks_a_tree(tmp_path: Path):
14
+ # a chain-dialect program: build a tree with injected primitives, walk it
15
+ src = """
16
+ tree = SkillTree(TreeNode("root", "sc", children=[TreeNode("a", "ac"), TreeNode("b", "cor")]))
17
+ materialize(tree, OUT)
18
+ result = reachable(OUT, "root")
19
+ """
20
+ out = str(tmp_path / "tree")
21
+ result = interpret(src, env={"OUT": out})
22
+ assert result == ["root", "a", "b"] # the execute arm followed the breadcrumbs
23
+
24
+
25
+ def test_interpret_constructs_a_language(tmp_path: Path):
26
+ from corcc.notation import EINSTEIN
27
+ src = """
28
+ result = construct_language("triage", ac_chain="[A] ⇒ [B] ⇒ |C|",
29
+ cor_persona=PERSONA, db=DB, skills_dir=SK, out_dir=OUT)
30
+ """
31
+ bundle = interpret(src, env={
32
+ "PERSONA": EINSTEIN, "DB": str(tmp_path / "x.db"),
33
+ "SK": str(tmp_path / "sk"), "OUT": str(tmp_path / "out"),
34
+ })
35
+ assert bundle.ac.exists() and bundle.cor.exists() and bundle.sc.exists()
36
+
37
+
38
+ def test_tree_to_mcp_writes_launcher(tmp_path: Path):
39
+ src = 'materialize(SkillTree(TreeNode("root","sc",children=[TreeNode("a","ac")])), OUT)'
40
+ out = str(tmp_path / "tree")
41
+ interpret(src, env={"OUT": out})
42
+ launcher = tree_to_mcp(out, "root", tmp_path / "serve_tree.py")
43
+ assert launcher.is_file()
44
+ text = launcher.read_text()
45
+ assert "build_tree_server" in text and "root" in text
46
+
47
+
48
+ def test_build_tree_server_constructs(tmp_path: Path):
49
+ src = 'materialize(SkillTree(TreeNode("root","sc",children=[TreeNode("a","ac")])), OUT)'
50
+ out = str(tmp_path / "tree")
51
+ interpret(src, env={"OUT": out})
52
+ server = build_tree_server(out, "root")
53
+ assert server is not None
54
+ # the runtime the server wraps works
55
+ assert reachable(out, "root") == ["root", "a"]
56
+ assert read_skill(out, "a") is not None
57
+
58
+
59
+ def test_walk_reaches_grandchildren_of_a_portable_tree(tmp_path: Path, monkeypatch):
60
+ # SkillTree ≥0.4.0 writes each breadcrumb relative to its node's folder; the walk must
61
+ # resolve it there, not against the working directory. A one-level tree hides the
62
+ # difference (a missing child still records its name), so this tree has two levels.
63
+ from si import walk
64
+ src = ('materialize(SkillTree(TreeNode("root","sc",children=['
65
+ 'TreeNode("a","cor",children=[TreeNode("a1","ac")])])), OUT)')
66
+ out = tmp_path / "tree"
67
+ interpret(src, env={"OUT": str(out)})
68
+ monkeypatch.chdir(tmp_path) # any working directory but the tree's own
69
+ assert reachable(out, "root") == ["root", "a", "a1"]
70
+
71
+ def errors(node):
72
+ own = [node["name"]] if "error" in node else []
73
+ return own + [e for child in node["children"] for e in errors(child)]
74
+
75
+ assert errors(walk(out, "root")) == []
76
+
77
+
78
+ def test_read_skill_finds_a_skill_in_a_coordinate_tree(tmp_path: Path):
79
+ # materialize(coords=True) names each folder `<coord>-<name>`; read_skill finds it by its name
80
+ from skilltree import SkillTree, TreeNode, materialize
81
+ out = tmp_path / "tree"
82
+ materialize(SkillTree(TreeNode("root", "sc", children=[TreeNode("a", "ac")])), out, coords=True)
83
+ assert read_skill(out, "a") is not None
84
+ assert read_skill(out, "missing") is None
85
+
@@ -0,0 +1,32 @@
1
+ """The self-interpreter's search door never crashes on what a query holds: one with no word in it finds nothing, and
2
+ an operator word (`NOT`, `AND`) is read as a word.
3
+
4
+ A query with no word crashes glyphsteer's index (the public repository's bug sweeps, pull requests #1 and #2), and both
5
+ crash SkillTree's search behind this door; the door sends SkillTree the query's words alone.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from si.server import si_search
12
+
13
+
14
+ def _tree(root: Path) -> Path:
15
+ skill = root / "triage" / "SKILL.md"
16
+ skill.parent.mkdir(parents=True)
17
+ skill.write_text("---\nname: triage\ndescription: triage some text\n---\nbody text\n", encoding="utf-8")
18
+ return root
19
+
20
+
21
+ def test_si_search_answers_a_query_with_no_word_with_nothing(tmp_path):
22
+ root = _tree(tmp_path)
23
+ for query in ('"', '?"!', '""', " "):
24
+ assert si_search(str(root), query) == []
25
+ assert [h["name"] for h in si_search(str(root), "triage")] == ["triage"]
26
+
27
+
28
+ def test_si_search_reads_operator_words_as_words(tmp_path):
29
+ root = _tree(tmp_path)
30
+ assert si_search(str(root), "NOT") == [] # FTS5's NOT, unquoted, was a syntax error
31
+ for query in ("triage AND", "Triage OR NOT", "NEAR triage", "triage*"):
32
+ assert [h["name"] for h in si_search(str(root), query)] == ["triage"], query