plexus-mesh 0.2.2__py3-none-any.whl

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.
plexus/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """plexus: capability discovery + auto-wiring for agent toolchains.
2
+
3
+ Point it at a set of tools that ship interop manifests and declarative discovery
4
+ wires producer to consumer into a pipeline, every edge tagged `declared` and
5
+ carrying the module its producer names. Discovery does not import or run cited
6
+ tools. Optional probe helpers in `plexus.registry` launch owned MCP servers only
7
+ when explicitly called. Zero runtime dependencies.
8
+ """
9
+ from .graph import to_dot, to_mermaid
10
+ from .manifest import Manifest, Port, validate
11
+ from .mesh import Edge, Mesh, discover
12
+ from .plan import plan_to, route
13
+ from .receipt import build_plan_receipt, verify_plan
14
+ from .registry import builtin_manifests, load_dir
15
+ from .run import pipeline_script
16
+
17
+ __version__ = "0.2.2"
18
+
19
+ __all__ = [
20
+ "Manifest", "Port", "validate",
21
+ "Mesh", "Edge", "discover",
22
+ "plan_to", "route",
23
+ "build_plan_receipt", "verify_plan",
24
+ "to_mermaid", "to_dot", "pipeline_script",
25
+ "builtin_manifests", "load_dir",
26
+ "__version__",
27
+ ]
plexus/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from plexus.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
plexus/cli.py ADDED
@@ -0,0 +1,139 @@
1
+ """cli.py — plexus command line.
2
+
3
+ plexus discover [--builtin] [--dir DIR] # the mesh: organs, edges, orphans
4
+ plexus wiring [...] # capability -> (producer, consumer) pairs
5
+ plexus plan --goal ORGAN [...] # upstream pipeline that feeds ORGAN
6
+ plexus route --from A --to B [...] # capability path from A to B
7
+ plexus validate [...] # manifest problems (exit 1 if any)
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ from datetime import datetime, timezone
14
+
15
+ from . import __version__
16
+ from .graph import to_dot, to_mermaid
17
+ from .manifest import content_hash, duplicate_organs, validate
18
+ from .mesh import discover
19
+ from .plan import plan_to, route
20
+ from .registry import builtin_manifests, export_all, load_dir
21
+ from .run import pipeline_script
22
+
23
+
24
+ def _load(args) -> list:
25
+ mans = list(builtin_manifests()) if (args.builtin or not args.dir) else []
26
+ if args.dir:
27
+ mans += load_dir(args.dir)
28
+ return mans
29
+
30
+
31
+ def _receipt(mesh) -> dict:
32
+ """A re-runnable provenance receipt for the mesh: which manifests were read,
33
+ from where, and a content hash a stranger can recompute from the same bytes."""
34
+ return {
35
+ "plexus_version": __version__,
36
+ "generated_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
37
+ "manifests": [{"organ": m.organ, "source": m.source,
38
+ "sha256": content_hash(m)}
39
+ for m in sorted(mesh.manifests.values(), key=lambda m: m.organ)],
40
+ }
41
+
42
+
43
+ def _mesh_json(mesh) -> dict:
44
+ return {
45
+ "organs": mesh.organs,
46
+ "edges": [{"producer": e.producer, "consumer": e.consumer,
47
+ "capability": e.capability, "self_loop": e.self_loop,
48
+ "via": e.producer_module, "evidence": e.evidence}
49
+ for e in mesh.edges],
50
+ "orphans": mesh.orphans(),
51
+ "collisions": mesh.collisions,
52
+ "receipt": _receipt(mesh),
53
+ }
54
+
55
+
56
+ def _add_source_flags(p):
57
+ p.add_argument("--builtin", action="store_true", help="include the built-in flagship manifests")
58
+ p.add_argument("--dir", default="", help="load *.interop.json manifests from this directory")
59
+
60
+
61
+ def main(argv: "list[str] | None" = None) -> int:
62
+ ap = argparse.ArgumentParser(prog="plexus", description=__doc__)
63
+ ap.add_argument("--version", action="version", version=f"plexus {__version__}")
64
+ sub = ap.add_subparsers(dest="cmd", required=True)
65
+
66
+ for name in ("discover", "wiring", "validate"):
67
+ sp = sub.add_parser(name)
68
+ _add_source_flags(sp)
69
+ pp = sub.add_parser("plan"); _add_source_flags(pp)
70
+ pp.add_argument("--goal", required=True, help="organ you want to feed")
71
+ rp = sub.add_parser("route"); _add_source_flags(rp)
72
+ rp.add_argument("--from", dest="src", required=True)
73
+ rp.add_argument("--to", dest="dst", required=True)
74
+ vp = sub.add_parser("verify"); _add_source_flags(vp)
75
+ vp.add_argument("--plan", required=True,
76
+ help="a plan/route JSON file (from `plexus plan`/`route`) to re-derive "
77
+ "and re-verify against the current mesh")
78
+ gp = sub.add_parser("graph"); _add_source_flags(gp)
79
+ gp.add_argument("--format", default="mermaid", choices=["mermaid", "dot"])
80
+ up = sub.add_parser("run"); _add_source_flags(up)
81
+ up.add_argument("--goal", required=True, help="organ you want to feed")
82
+ ep = sub.add_parser("export")
83
+ ep.add_argument("--dir", default="manifests", help="write <organ>.interop.json files here")
84
+ sub.add_parser("mcp") # stdio MCP server: discover/wiring/plan/route as tools
85
+
86
+ args = ap.parse_args(argv)
87
+
88
+ if args.cmd == "mcp":
89
+ from .mcp import serve
90
+ return serve()
91
+ if args.cmd == "export":
92
+ print("\n".join(export_all(args.dir)))
93
+ return 0
94
+
95
+ mans = _load(args)
96
+
97
+ if args.cmd == "discover":
98
+ print(json.dumps(_mesh_json(discover(mans)), indent=2))
99
+ return 0
100
+ if args.cmd == "wiring":
101
+ print(json.dumps(discover(mans).wiring(), indent=2))
102
+ return 0
103
+ if args.cmd == "plan":
104
+ print(json.dumps(plan_to(discover(mans), args.goal), indent=2))
105
+ return 0
106
+ if args.cmd == "route":
107
+ print(json.dumps(route(discover(mans), args.src, args.dst), indent=2))
108
+ return 0
109
+ if args.cmd == "verify":
110
+ from .receipt import verify_plan
111
+ with open(args.plan, encoding="utf-8") as f:
112
+ plan = json.load(f)
113
+ ok = verify_plan(plan, discover(mans))
114
+ print(json.dumps({"verified": ok, "target": plan.get("target"),
115
+ "source": plan.get("source")}, indent=2))
116
+ return 0 if ok else 1
117
+ if args.cmd == "graph":
118
+ mesh = discover(mans)
119
+ print(to_mermaid(mesh) if args.format == "mermaid" else to_dot(mesh))
120
+ return 0
121
+ if args.cmd == "run":
122
+ print(pipeline_script(discover(mans), args.goal))
123
+ return 0
124
+ if args.cmd == "validate":
125
+ problems = {m.organ: validate(m) for m in mans}
126
+ report = {k: v for k, v in problems.items() if v}
127
+ dups = duplicate_organs(mans)
128
+ if dups:
129
+ # A set-level clash the per-organ dict above cannot show (its own key
130
+ # collapses the duplicates); name it so a collision can never launder
131
+ # to {"ok": true}.
132
+ report["duplicate_organs"] = dups
133
+ print(json.dumps(report or {"ok": True}, indent=2))
134
+ return 1 if report else 0
135
+ return 2
136
+
137
+
138
+ if __name__ == "__main__":
139
+ raise SystemExit(main())
plexus/graph.py ADDED
@@ -0,0 +1,38 @@
1
+ """graph.py — render the mesh as a diagram (Mermaid / Graphviz DOT).
2
+
3
+ Discovery is only useful if you can see it. This turns the mesh into a picture:
4
+ organs as nodes, capability-labeled edges as producer -> consumer arrows, with
5
+ self-loops and the cross-tool wiring visible at a glance. Mermaid renders in any
6
+ Markdown viewer; DOT feeds Graphviz. Pure string output, zero deps.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from .mesh import Mesh
11
+
12
+
13
+ def _san(node: str) -> str:
14
+ """A Mermaid/DOT-safe node id (organs are simple slugs already, but be safe)."""
15
+ return "".join(c if c.isalnum() else "_" for c in node)
16
+
17
+
18
+ def to_mermaid(mesh: Mesh, *, direction: str = "LR") -> str:
19
+ """A Mermaid flowchart. Cross-tool edges are solid; self-loops dotted."""
20
+ lines = [f"flowchart {direction}"]
21
+ for organ in mesh.organs:
22
+ lines.append(f' {_san(organ)}["{organ}"]')
23
+ for e in mesh.edges:
24
+ arrow = "-.->" if e.self_loop else "-->"
25
+ lines.append(f' {_san(e.producer)} {arrow}|"{e.capability}"| {_san(e.consumer)}')
26
+ return "\n".join(lines)
27
+
28
+
29
+ def to_dot(mesh: Mesh) -> str:
30
+ """A Graphviz DOT digraph."""
31
+ lines = ["digraph plexus {", ' rankdir=LR;', ' node [shape=box];']
32
+ for organ in mesh.organs:
33
+ lines.append(f' "{organ}";')
34
+ for e in mesh.edges:
35
+ attrs = f'label="{e.capability}"' + (", style=dotted" if e.self_loop else "")
36
+ lines.append(f' "{e.producer}" -> "{e.consumer}" [{attrs}];')
37
+ lines.append("}")
38
+ return "\n".join(lines)
plexus/manifest.py ADDED
@@ -0,0 +1,117 @@
1
+ """manifest.py — what a tool declares about how it plugs into others.
2
+
3
+ A manifest is a tool's interop contract: the capabilities it EMITS (re-checkable
4
+ artifacts another tool could consume) and the capabilities it CONSUMES (inputs it
5
+ accepts). A capability is a stable string: a schema id like
6
+ "mneme.crucible-export/2" or a shared-spine kind like
7
+ "project-telos.flagship-action/v1". Matching is by capability string, so an edge
8
+ forms when a producer DECLARES a capability the consumer DECLARES it accepts.
9
+
10
+ Every port carries `module` (a file:function pointer) the producer names as the
11
+ source. plexus does not import, resolve, or run that pointer, so the mesh is
12
+ DECLARED, not probed: the module is a self-reported citation to follow, not a
13
+ verified receipt. A manifest is a plain dict/JSON, so any tool in any language
14
+ can ship one; nothing here imports the tools it describes.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import json
20
+ from dataclasses import dataclass, field
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Port:
25
+ """One capability a tool emits or consumes."""
26
+ capability: str # the match key (schema id or shared kind)
27
+ title: str = "" # human label
28
+ module: str = "" # evidence: repo-relative file:function
29
+ summary: str = ""
30
+ consumable_as: tuple = () # extra capabilities an EMIT also satisfies
31
+ # (e.g. mneme.crucible-export/2 -> crucible.thesis)
32
+
33
+ def satisfies(self, capability: str) -> bool:
34
+ return capability == self.capability or capability in self.consumable_as
35
+
36
+
37
+ @dataclass
38
+ class Manifest:
39
+ organ: str
40
+ invoke: dict = field(default_factory=dict) # {cli, mcp_server, python_import}
41
+ emits: list = field(default_factory=list) # list[Port]
42
+ consumes: list = field(default_factory=list) # list[Port]
43
+ evidence: list = field(default_factory=list) # file paths the manifest was grounded on
44
+ source: str = "" # provenance: "builtin:registry" or the file path read
45
+
46
+ @staticmethod
47
+ def from_dict(d: dict) -> "Manifest":
48
+ def ports(key):
49
+ out = []
50
+ for p in d.get(key, []):
51
+ if isinstance(p, str):
52
+ out.append(Port(capability=p))
53
+ elif isinstance(p, dict):
54
+ out.append(Port(
55
+ capability=p.get("capability", ""), title=p.get("title", ""),
56
+ module=p.get("module", ""), summary=p.get("summary", ""),
57
+ consumable_as=tuple(p.get("consumable_as", ()))))
58
+ return out
59
+ # Tolerant by contract: a missing organ or capability becomes an empty
60
+ # string that validate() reports; from_dict never raises on malformed input.
61
+ return Manifest(organ=d.get("organ", ""), invoke=d.get("invoke", {}),
62
+ emits=ports("emits"), consumes=ports("consumes"),
63
+ evidence=list(d.get("evidence", [])))
64
+
65
+ def to_dict(self) -> dict:
66
+ def dump(ports):
67
+ return [{"capability": p.capability, "title": p.title, "module": p.module,
68
+ "summary": p.summary, "consumable_as": list(p.consumable_as)}
69
+ for p in ports]
70
+ return {"organ": self.organ, "invoke": self.invoke,
71
+ "emits": dump(self.emits), "consumes": dump(self.consumes),
72
+ "evidence": self.evidence}
73
+
74
+
75
+ def content_hash(m) -> str:
76
+ """sha256 over the manifest's canonical content (its to_dict, sorted keys).
77
+ Source is deliberately excluded, so the hash binds WHAT was declared, not
78
+ where it was read; a stranger can recompute it from the same bytes."""
79
+ canonical = json.dumps(m.to_dict(), sort_keys=True, separators=(",", ":"))
80
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
81
+
82
+
83
+ def duplicate_organs(manifests: list) -> list:
84
+ """Organ ids declared by more than one manifest in the set, sorted unique.
85
+ A set-level check: identity is self-declared, so a collision must be NAMED,
86
+ never silently resolved last-writer-wins."""
87
+ seen, dups = set(), set()
88
+ for m in manifests:
89
+ if m.organ in seen:
90
+ dups.add(m.organ)
91
+ seen.add(m.organ)
92
+ return sorted(dups)
93
+
94
+
95
+ def validate(m) -> list:
96
+ """Problems with a manifest (empty list == valid). A validator reports
97
+ malformed input; it never raises on it."""
98
+ issues = []
99
+ if not isinstance(m, Manifest):
100
+ return ["not a Manifest"]
101
+ if not m.organ or not isinstance(m.organ, str):
102
+ issues.append("organ id missing or not a string")
103
+ seen = set()
104
+ for side in ("emits", "consumes"):
105
+ for i, p in enumerate(getattr(m, side)):
106
+ if not isinstance(p, Port) or not p.capability:
107
+ issues.append(f"{side}[{i}] has no capability")
108
+ continue
109
+ if side == "emits" and not p.module:
110
+ # The producing module is the evidence the "cites its module"
111
+ # claim rests on; an emit without it is a receipt-less claim.
112
+ issues.append(f"emits[{i}] {p.capability!r} has no module evidence")
113
+ key = (side, p.capability)
114
+ if key in seen:
115
+ issues.append(f"{side} declares {p.capability!r} twice")
116
+ seen.add(key)
117
+ return issues
plexus/mcp.py ADDED
@@ -0,0 +1,156 @@
1
+ """mcp.py — a zero-dep MCP server so any agent can query the mesh live.
2
+
3
+ A CLI mesh is useful to a human; an agent needs it mid-task. This exposes
4
+ discover / wiring / plan / route as MCP tools over stdio JSON-RPC 2.0 (the same
5
+ shape every flagship speaks), so a harness can ask "what feeds crucible?" or
6
+ "how do gather and crucible connect?" while it works. No SDK, stdlib only.
7
+
8
+ The transport (`serve`) is a thin loop; all logic is in `handle`, which maps a
9
+ request dict to a response dict and is fully testable without any pipes.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import sys
15
+
16
+ from . import __version__
17
+ from .mesh import discover
18
+ from .plan import plan_to, route
19
+ from .registry import builtin_manifests, load_dir
20
+
21
+ PROTOCOL = "2025-06-18"
22
+
23
+ _DIR = {"dir": {"type": "string", "description": "load extra *.interop.json manifests from here"}}
24
+
25
+ TOOLS = [
26
+ {"name": "plexus_discover",
27
+ "description": "Discover the interop mesh: organs, producer->consumer edges (each with the module that produces it), and orphans (unmet inputs / unconsumed outputs).",
28
+ "inputSchema": {"type": "object", "properties": dict(_DIR)}},
29
+ {"name": "plexus_wiring",
30
+ "description": "The wiring map: capability -> list of (producer, consumer) pairs.",
31
+ "inputSchema": {"type": "object", "properties": dict(_DIR)}},
32
+ {"name": "plexus_plan",
33
+ "description": "The upstream pipeline that feeds a target organ, in dependency order, with any feedback loops reported.",
34
+ "inputSchema": {"type": "object", "required": ["goal"],
35
+ "properties": {"goal": {"type": "string", "description": "organ to feed"}, **_DIR}}},
36
+ {"name": "plexus_route",
37
+ "description": "The shortest capability path from one organ to another.",
38
+ "inputSchema": {"type": "object", "required": ["source", "target"],
39
+ "properties": {"source": {"type": "string"}, "target": {"type": "string"}, **_DIR}}},
40
+ {"name": "plexus.status",
41
+ "description": "Liveness and identity of the plexus MCP server (name, version, protocol). Network-free health probe.",
42
+ "inputSchema": {"type": "object", "properties": {}}},
43
+ {"name": "plexus.doctor",
44
+ "description": "Readiness diagnostic: identity plus the count of builtin interop manifests and the tools exposed.",
45
+ "inputSchema": {"type": "object", "properties": {}}},
46
+ ]
47
+
48
+
49
+ def _mesh(args: dict):
50
+ mans = list(builtin_manifests())
51
+ if args.get("dir"):
52
+ mans += load_dir(args["dir"])
53
+ return discover(mans)
54
+
55
+
56
+ def _mesh_json(mesh) -> dict:
57
+ return {"organs": mesh.organs,
58
+ "edges": [{"producer": e.producer, "consumer": e.consumer,
59
+ "capability": e.capability, "self_loop": e.self_loop,
60
+ "via": e.producer_module} for e in mesh.edges],
61
+ "orphans": mesh.orphans()}
62
+
63
+
64
+ def _call(params: dict) -> dict:
65
+ name, args = params.get("name"), params.get("arguments", {}) or {}
66
+ if name in ("plexus.status", "plexus.doctor"):
67
+ info = {"ok": True, "server": "plexus", "version": __version__, "protocol": PROTOCOL}
68
+ if name == "plexus.doctor":
69
+ info["builtin_manifests"] = len(list(builtin_manifests()))
70
+ info["tools"] = [t["name"] for t in TOOLS]
71
+ return {"content": [{"type": "text", "text": json.dumps(info, indent=2)}]}
72
+ try:
73
+ mesh = _mesh(args)
74
+ if name == "plexus_discover":
75
+ result = _mesh_json(mesh)
76
+ elif name == "plexus_wiring":
77
+ result = mesh.wiring()
78
+ elif name == "plexus_plan":
79
+ result = plan_to(mesh, args.get("goal", ""))
80
+ elif name == "plexus_route":
81
+ result = route(mesh, args.get("source", ""), args.get("target", ""))
82
+ else:
83
+ return {"content": [{"type": "text", "text": f"unknown tool {name!r}"}], "isError": True}
84
+ except Exception as e: # a tool error is a result, not a crash
85
+ return {"content": [{"type": "text", "text": f"[error] {type(e).__name__}: {e}"}],
86
+ "isError": True}
87
+ return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
88
+
89
+
90
+ def _ok(rid, result):
91
+ return {"jsonrpc": "2.0", "id": rid, "result": result}
92
+
93
+
94
+ def _err(rid, code, message):
95
+ return {"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": message}}
96
+
97
+
98
+ def _valid_request_id(value) -> bool:
99
+ return type(value) is str or type(value) is int
100
+
101
+
102
+ def _safe_request_id(req: dict):
103
+ if "id" not in req:
104
+ return None
105
+ rid = req["id"]
106
+ return rid if _valid_request_id(rid) else None
107
+
108
+
109
+ def handle(req: dict):
110
+ """Map one JSON-RPC request to a response dict (or None for notifications)."""
111
+ if not isinstance(req, dict):
112
+ return _err(None, -32600, "invalid request")
113
+ rid = _safe_request_id(req)
114
+ if "id" in req and not _valid_request_id(req["id"]):
115
+ return _err(None, -32600, "invalid request")
116
+ if req.get("jsonrpc") != "2.0":
117
+ return _err(rid, -32600, "invalid request")
118
+ method = req.get("method")
119
+ if not isinstance(method, str):
120
+ return _err(rid, -32600, "invalid request: method must be a string")
121
+ if "id" not in req:
122
+ return None
123
+ if method == "initialize":
124
+ return _ok(rid, {"protocolVersion": PROTOCOL, "capabilities": {"tools": {}},
125
+ "serverInfo": {"name": "plexus", "version": __version__}})
126
+ if method == "tools/list":
127
+ return _ok(rid, {"tools": TOOLS})
128
+ if method == "tools/call":
129
+ params = req["params"] if "params" in req else {}
130
+ if not isinstance(params, dict):
131
+ return _err(rid, -32602, "invalid params")
132
+ return _ok(rid, _call(params))
133
+ return _err(rid, -32601, f"method not found: {method}")
134
+
135
+
136
+ def serve(stdin=None, stdout=None) -> int:
137
+ stdin, stdout = stdin or sys.stdin, stdout or sys.stdout
138
+ for line in stdin:
139
+ line = line.strip()
140
+ if not line:
141
+ continue
142
+ try:
143
+ req = json.loads(line)
144
+ except json.JSONDecodeError:
145
+ stdout.write(json.dumps(_err(None, -32700, "parse error")) + "\n")
146
+ stdout.flush()
147
+ continue
148
+ resp = handle(req)
149
+ if resp is not None:
150
+ stdout.write(json.dumps(resp) + "\n")
151
+ stdout.flush()
152
+ return 0
153
+
154
+
155
+ if __name__ == "__main__":
156
+ raise SystemExit(serve())
plexus/mesh.py ADDED
@@ -0,0 +1,88 @@
1
+ """mesh.py — discover the wiring: whose output plugs into whose input.
2
+
3
+ Given a set of manifests, the mesh computes producer -> consumer EDGES: for every
4
+ capability a tool consumes, it finds the tools that emit something satisfying it.
5
+ This is the layer MCP tool lists lack: MCP tells an agent that tools exist; the
6
+ mesh tells it how their OUTPUTS chain into each other's INPUTS.
7
+
8
+ An edge is DECLARED, not probed. It forms by matching capability strings the
9
+ manifests self-report; plexus never imports, resolves, or runs the tools, so it
10
+ cannot confirm the cited module exists or that the outputs truly compose. Every
11
+ edge is tagged `evidence="declared"` and carries the module its producer names,
12
+ a self-reported citation to follow, not a verified receipt.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+
18
+ from .manifest import duplicate_organs
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Edge:
23
+ producer: str # organ id
24
+ consumer: str # organ id
25
+ capability: str # the consumer's input capability that the producer satisfies
26
+ producer_module: str
27
+ consumer_module: str
28
+ self_loop: bool = False
29
+ evidence: str = "declared" # "declared": self-reported, never probed by plexus
30
+
31
+
32
+ @dataclass
33
+ class Mesh:
34
+ organs: list # organ ids, sorted
35
+ edges: list # list[Edge]
36
+ manifests: dict # organ -> Manifest (deduped, last-writer-wins)
37
+ collisions: list = field(default_factory=list) # organ ids declared more than once
38
+
39
+ def wiring(self) -> dict:
40
+ """capability -> sorted list of (producer, consumer) pairs."""
41
+ out: dict = {}
42
+ for e in self.edges:
43
+ out.setdefault(e.capability, []).append((e.producer, e.consumer))
44
+ return {k: sorted(v) for k, v in sorted(out.items())}
45
+
46
+ def producers_of(self, organ: str) -> list:
47
+ """Organs whose output feeds `organ` (its upstream), sorted unique."""
48
+ return sorted({e.producer for e in self.edges
49
+ if e.consumer == organ and not e.self_loop})
50
+
51
+ def consumers_of(self, organ: str) -> list:
52
+ """Organs that consume `organ`'s output (its downstream), sorted unique."""
53
+ return sorted({e.consumer for e in self.edges
54
+ if e.producer == organ and not e.self_loop})
55
+
56
+ def orphans(self) -> dict:
57
+ """Honesty surface: capabilities consumed by someone but emitted by no
58
+ organ in the set (an unmet input), and emitted but never consumed."""
59
+ emitted, consumed = set(), set()
60
+ for m in self.manifests.values():
61
+ for p in m.emits:
62
+ emitted.add(p.capability)
63
+ emitted.update(p.consumable_as)
64
+ for p in m.consumes:
65
+ consumed.add(p.capability)
66
+ return {"unmet_inputs": sorted(consumed - emitted),
67
+ "unconsumed_outputs": sorted(emitted - consumed)}
68
+
69
+
70
+ def discover(manifests: list) -> Mesh:
71
+ """Build the mesh. An edge (A -> B, cap) exists when B consumes capability
72
+ `cap` and A emits a port that satisfies `cap`."""
73
+ mans = {m.organ: m for m in manifests}
74
+ edges = []
75
+ for consumer in manifests:
76
+ for want in consumer.consumes:
77
+ for producer in manifests:
78
+ for emit in producer.emits:
79
+ if emit.satisfies(want.capability):
80
+ edges.append(Edge(
81
+ producer=producer.organ, consumer=consumer.organ,
82
+ capability=want.capability,
83
+ producer_module=emit.module, consumer_module=want.module,
84
+ self_loop=(producer.organ == consumer.organ),
85
+ evidence="declared"))
86
+ edges.sort(key=lambda e: (e.capability, e.producer, e.consumer))
87
+ return Mesh(organs=sorted(mans), edges=edges, manifests=mans,
88
+ collisions=duplicate_organs(manifests))