feat(cli): edit role bonds in a matrix instead of hunting through files

A bond lives on the consumer's side of a service relation, so seeing how two
roles couple means opening both roles' meta/services.yml and comparing, and
retuning one means editing a scalar buried among dozens of unrelated keys.

Add `infinito meta roles applications bond` (make bond / i8bond): it resolves
every services.<key>.bond to a role-to-role edge through the service registry
and serves the result as a matrix, each cell carrying both directions because
the two need not agree. Brightness is the bond, black for none and white for
1, so the coupling of the whole repository reads at a glance.

Clicking a cell writes the new value back to the declaring role. The write is
line surgery rather than a YAML round-trip: these files carry `# nocheck:`
directives and hand-tuned ordering that re-dumping would flatten, so only the
bond scalar moves and a trailing comment on that line survives. The browser
sends a role pair, never a path, and the server resolves the service key from
its own edge map, so a pair it does not know is refused before any write.

`--out FILE` exports a read-only page and `--format json` the raw edges, for
callers that want to post-process rather than edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-02 00:19:43 +02:00
1 parent 8cc9c86f8a
commit f320d64836
11 files changed
+696

No files matched your search

Whitespace-only changes.
@@ -0,0 +1,76 @@
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from cli.meta.roles.applications.bond.edit import EditError, parse_bond, set_bond
from utils.roles.mapping import ROLE_FILE_META_SERVICES
SOURCE = """---
litellm:
bond: 0.5
enabled: "{{ 'svc-ai-litellm' in group_names }}"
# nocheck: dockerfile-custom
sso:
bond: 1 # nocheck: dynamic-flag
flavor: oidc
ldap:
enabled: true
"""
class TestSetBond(unittest.TestCase):
def setUp(self) -> None:
self._tmp = TemporaryDirectory()
self.roles = Path(self._tmp.name)
self.path = self.roles / "web-app-demo" / ROLE_FILE_META_SERVICES
self.path.parent.mkdir(parents=True)
self.path.write_text(SOURCE, encoding="utf-8")
self.addCleanup(self._tmp.cleanup)
def read(self) -> str:
return self.path.read_text(encoding="utf-8") # nocheck: cache-read — re-reads the file set_bond just rewrote
def test_rewrites_the_value_in_place(self) -> None:
self.assertEqual(set_bond(self.roles, "web-app-demo", "litellm", 0.25), "0.25")
self.assertIn(" bond: 0.25\n", self.read())
self.assertIn("# nocheck: dockerfile-custom\n", self.read())
def test_keeps_a_trailing_comment(self) -> None:
set_bond(self.roles, "web-app-demo", "sso", 0.5)
self.assertIn(" bond: 0.5 # nocheck: dynamic-flag\n", self.read())
def test_inserts_a_missing_bond(self) -> None:
set_bond(self.roles, "web-app-demo", "ldap", 1)
self.assertIn("ldap:\n bond: 1\n enabled: true\n", self.read())
def test_removes_on_none(self) -> None:
self.assertEqual(set_bond(self.roles, "web-app-demo", "litellm", None), "")
self.assertNotIn("bond: 0.5", self.read())
self.assertIn("litellm:\n enabled:", self.read())
def test_rejects_an_unknown_key(self) -> None:
with self.assertRaises(EditError):
set_bond(self.roles, "web-app-demo", "nope", 1)
def test_does_not_match_a_nested_key(self) -> None:
with self.assertRaises(EditError):
set_bond(self.roles, "web-app-demo", "flavor", 1)
class TestParseBond(unittest.TestCase):
def test_blank_clears(self) -> None:
self.assertIsNone(parse_bond(""))
self.assertIsNone(parse_bond(" - "))
def test_rejects_out_of_range(self) -> None:
for raw in ("2", "-0.1", "abc"):
with self.assertRaises(EditError):
parse_bond(raw)
def test_accepts_the_edges(self) -> None:
self.assertEqual(parse_bond("0"), 0.0)
self.assertEqual(parse_bond("1"), 1.0)
if __name__ == "__main__":
unittest.main()