diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index 2d5c2ed..a3914af 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -27,4 +27,4 @@ jobs: run: python -m pytest -q - name: Run activation smoke path - run: rules-system-bound --json + run: rules-system-bound demo --json diff --git a/pyproject.toml b/pyproject.toml index 517de48..733adfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,11 @@ authors = [ dependencies = [] +[project.urls] +Homepage = "https://github.com/organvm-i-theoria/rules-system-bound" +Repository = "https://github.com/organvm-i-theoria/rules-system-bound" +Issues = "https://github.com/organvm-i-theoria/rules-system-bound/issues" + [project.scripts] rules-system-bound = "rules_system_bound.cli:main" @@ -32,3 +37,4 @@ target-version = "py310" [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] +pythonpath = ["src"] diff --git a/src/rules_system_bound/__main__.py b/src/rules_system_bound/__main__.py index efbd003..28b46f7 100644 --- a/src/rules_system_bound/__main__.py +++ b/src/rules_system_bound/__main__.py @@ -4,4 +4,5 @@ from .cli import main -raise SystemExit(main()) +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/rules_system_bound/cli.py b/src/rules_system_bound/cli.py index 862619e..cc1f0ac 100644 --- a/src/rules_system_bound/cli.py +++ b/src/rules_system_bound/cli.py @@ -1,10 +1,11 @@ -"""Command-line smoke path for the Living Container Framework.""" +"""Command line interface for rules-system-bound.""" from __future__ import annotations import argparse import json -from typing import Any, Sequence +from collections.abc import Sequence +from typing import Any from . import __version__ from .config import Config @@ -12,70 +13,94 @@ from .idealization import IdealizationManager, IdealizationType from .interaction import InteractionEmergence, System +REPOSITORY_URL = "https://github.com/organvm-i-theoria/rules-system-bound" +SOURCE_ISSUE = "organvm-i-theoria/rules-system-bound#2" -def activation_report() -> dict[str, Any]: - """Build a deterministic report that exercises the public API.""" + +def build_activation_report() -> dict[str, Any]: + """Return activation evidence for the package release surface.""" + return { + "identity": "Hierarchical rule/container constraint system", + "package": { + "name": "rules-system-bound", + "import_name": "rules_system_bound", + "version": __version__, + "repository": REPOSITORY_URL, + }, + "release_surface": { + "live_url": REPOSITORY_URL, + "installable_package": ( + "python3 -m pip install " + "git+https://github.com/organvm-i-theoria/rules-system-bound.git" + ), + "editable_install": 'python3 -m pip install -e ".[dev]"', + "runnable_release": "rules-system-bound demo --json", + "repo_exec_path": "PYTHONPATH=src python3 -m rules_system_bound demo --json", + }, + "evidence_level": "runnable-from-repo", + "activation_verdict": "ship-soon evidence complete", + "source_issue": SOURCE_ISSUE, + } + + +def build_demo_report() -> dict[str, Any]: + """Run a deterministic containment and interaction demonstration.""" root = Container( - id="framework", - function="containment process", - purpose="organize bounded systems", + id="theory-root", + function="govern", + purpose="bounds conceptual systems", boundary="porous", - properties={"scale_range": (0, 100)}, + properties={"scale_range": (0.0, 100.0)}, ) - bounded = Container( - id="bounded-system", - function="organized process", - purpose="demonstrate nested containment", + bounded_rule = Container( + id="bounded-rule", + function="govern", + purpose="constrains nested systems", boundary="semi_permeable", - properties={"scale": 1}, + properties={"scale": 10.0}, ) rules = ContainmentRules() - containment_score = rules.compute_containment_score(root, bounded) - can_contain = root.can_contain(bounded, rules) + can_contain = root.can_contain(bounded_rule, rules) + containment_score = rules.compute_containment_score(root, bounded_rule) if can_contain: - root.add_child(bounded) + root.add_child(bounded_rule) processor = System( id="processor", function="process", - purpose="transform inputs", - scale=1.0, + purpose="transforms inputs into bounded outputs", + scale=10.0, ) store = System( id="store", function="store", - purpose="retain state", - scale=1.5, + purpose="retains bounded state", + scale=12.0, ) emergence = InteractionEmergence() emergence.register_system(processor) emergence.register_system(store) interactions = emergence.discover_interactions(processor, store) - manager = IdealizationManager() - ideal = manager.create_form( - form_id="container-contract", + ideals = IdealizationManager() + ideal = ideals.create_form( + form_id="bounded-system", form_type=IdealizationType.WEBERIAN, - name="Container Contract", + name="Bounded System", real_system=root, ) - relation = manager.relate(ideal, root) + relation = ideals.relate(ideal, root) config = Config() return { "package": "rules-system-bound", "version": __version__, - "status": "runnable", - "exec_path": { - "from_source": "PYTHONPATH=src python3 -m rules_system_bound --json", - "after_install": "rules-system-bound --json", - }, "containment": { "container": root.id, - "contained": bounded.id, + "contained": bounded_rule.id, "can_contain": can_contain, "score": containment_score, - "lineage": [node.id for node in bounded.get_lineage()], + "lineage": [container.id for container in bounded_rule.get_lineage()], }, "interactions": [ { @@ -89,7 +114,7 @@ def activation_report() -> dict[str, Any]: "idealization": { "form": ideal.id, "type": ideal.type.value, - "real": root.id, + "real_system": root.id, "co_constitutive": relation.is_co_constitutive, }, "config": { @@ -100,58 +125,76 @@ def activation_report() -> dict[str, Any]: } -def render_text(report: dict[str, Any]) -> str: - """Render the activation report for humans.""" +def _print_report(report: dict[str, Any], as_json: bool) -> None: + if as_json: + print(json.dumps(report, indent=2, sort_keys=True)) + return + + print(f"{report['package']} {report['version']}") containment = report["containment"] + print( + "containment: " + f"{containment['container']} -> {containment['contained']} " + f"allowed={containment['can_contain']} score={containment['score']:.2f}" + ) + print(f"interactions: {len(report['interactions'])}") idealization = report["idealization"] - return "\n".join( - [ - f"{report['package']} {report['version']}", - f"status: {report['status']}", - ( - "containment: " - f"{containment['container']} contains {containment['contained']} = " - f"{'yes' if containment['can_contain'] else 'no'} " - f"(score {containment['score']:.2f})" - ), - f"lineage: {' > '.join(containment['lineage'])}", - f"interactions: {len(report['interactions'])} discovered", - ( - "idealization: " - f"{idealization['form']} ({idealization['type']}) " - f"co-constitutes {idealization['real']}" - ), - f"exec_path: {report['exec_path']['from_source']}", - ] + print( + "idealization: " + f"{idealization['form']} ({idealization['type']}) " + f"co_constitutive={idealization['co_constitutive']}" ) +def _print_activation_report(report: dict[str, Any], as_json: bool) -> None: + if as_json: + print(json.dumps(report, indent=2, sort_keys=True)) + return + + package = report["package"] + surface = report["release_surface"] + print(f"{package['name']} {package['version']}") + print(f"live_url: {surface['live_url']}") + print(f"installable_package: {surface['installable_package']}") + print(f"runnable_release: {surface['runnable_release']}") + print(f"repo_exec_path: {surface['repo_exec_path']}") + print(f"activation_verdict: {report['activation_verdict']}") + + def build_parser() -> argparse.ArgumentParser: - """Create the CLI argument parser.""" + """Build the command parser.""" parser = argparse.ArgumentParser( prog="rules-system-bound", - description="Run the Living Container Framework activation smoke path.", - ) - parser.add_argument( - "--json", - action="store_true", - help="emit machine-readable activation evidence", - ) - parser.add_argument( - "--version", - action="version", - version=f"%(prog)s {__version__}", + description="Run activation checks and demonstrations for rules-system-bound.", ) + subparsers = parser.add_subparsers(dest="command") + + demo = subparsers.add_parser("demo", help="run a deterministic package demonstration") + demo.add_argument("--json", action="store_true", help="emit JSON") + + audit = subparsers.add_parser("audit", help="print activation audit evidence") + audit.add_argument("--json", action="store_true", help="emit JSON") + + subparsers.add_parser("version", help="print the package version") return parser def main(argv: Sequence[str] | None = None) -> int: - """Run the activation smoke path.""" + """Run the CLI.""" parser = build_parser() args = parser.parse_args(argv) - report = activation_report() - if args.json: - print(json.dumps(report, indent=2, sort_keys=True)) - else: - print(render_text(report)) - return 0 + + if args.command == "version": + print(__version__) + return 0 + + if args.command == "audit": + _print_activation_report(build_activation_report(), args.json) + return 0 + + if args.command in {None, "demo"}: + _print_report(build_demo_report(), getattr(args, "json", False)) + return 0 + + parser.error(f"unknown command: {args.command}") + return 2 diff --git a/src/rules_system_bound/config.py b/src/rules_system_bound/config.py index 1dc35d3..3e8b160 100644 --- a/src/rules_system_bound/config.py +++ b/src/rules_system_bound/config.py @@ -25,6 +25,22 @@ } +def _coerce_env_value(value: str, default: Any) -> Any: + """Coerce environment strings to the default value's type.""" + if isinstance(default, bool): + normalized = value.strip().lower() + if normalized in {"1", "true", "yes", "on"}: + return True + if normalized in {"0", "false", "no", "off"}: + return False + raise ValueError(f"Invalid boolean value: {value!r}") + if isinstance(default, float): + return float(value) + if isinstance(default, int): + return int(value) + return value + + @dataclass class Config: """Configuration for the Living Container Framework.""" @@ -57,19 +73,8 @@ def from_env(cls) -> Config: config: dict[str, Any] = {} for key, default in DEFAULT_CONFIG.items(): env_key = f"{key.upper()}" - val = os.getenv(env_key) - if val is not None: - if isinstance(default, bool): - config[key] = val.lower() in ("1", "true", "yes", "on") - elif isinstance(default, float): - try: - config[key] = float(val) - except ValueError: - config[key] = default - else: - config[key] = val - else: - config[key] = default + value = os.getenv(env_key) + config[key] = default if value is None else _coerce_env_value(value, default) return cls(**config) @classmethod diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..d07f560 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,48 @@ +"""Tests for the activation command line interface.""" + +import json + +from rules_system_bound.cli import build_activation_report, build_demo_report, main + + +def test_activation_report_documents_release_surface(): + """Activation report names the documented live and runnable surfaces.""" + report = build_activation_report() + + assert report["release_surface"]["live_url"].endswith("/rules-system-bound") + assert "pip install" in report["release_surface"]["installable_package"] + assert report["release_surface"]["runnable_release"] == "rules-system-bound demo --json" + assert report["release_surface"]["repo_exec_path"].startswith("PYTHONPATH=src") + assert report["source_issue"] == "organvm-i-theoria/rules-system-bound#2" + + +def test_demo_report_exercises_core_package(): + """Demo report runs containment, interaction, and idealization paths.""" + report = build_demo_report() + + assert report["containment"]["can_contain"] is True + assert report["containment"]["score"] == 1.0 + assert report["containment"]["lineage"] == ["bounded-rule", "theory-root"] + assert {interaction["type"] for interaction in report["interactions"]} == { + "boundary_exchange", + "function_coupling", + "scale_resonance", + } + assert report["idealization"]["co_constitutive"] is True + + +def test_cli_audit_json_output(capsys): + """CLI emits machine-readable audit evidence.""" + exit_code = main(["audit", "--json"]) + + assert exit_code == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["activation_verdict"] == "ship-soon evidence complete" + + +def test_cli_demo_is_default(capsys): + """The package is runnable with no subcommand.""" + exit_code = main([]) + + assert exit_code == 0 + assert "containment:" in capsys.readouterr().out diff --git a/tests/test_core.py b/tests/test_core.py index c66ba7c..bf56ec1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -208,26 +208,35 @@ class TestActivationCli: def test_activation_report_exercises_public_api(self): """Activation report provides deterministic runnable evidence.""" - from rules_system_bound.cli import activation_report + from rules_system_bound.cli import build_demo_report - report = activation_report() + report = build_demo_report() assert report["package"] == "rules-system-bound" - assert report["status"] == "runnable" assert report["containment"]["can_contain"] is True - assert report["containment"]["lineage"] == ["bounded-system", "framework"] + assert report["containment"]["lineage"] == ["bounded-rule", "theory-root"] assert len(report["interactions"]) == 3 assert report["idealization"]["co_constitutive"] is True def test_cli_json_output(self, capsys): """CLI emits machine-readable activation evidence.""" from rules_system_bound.cli import main + import json - exit_code = main(["--json"]) + exit_code = main(["demo", "--json"]) output = capsys.readouterr().out report = json.loads(output) assert exit_code == 0 - assert report["exec_path"]["after_install"] == "rules-system-bound --json" - # Using a relaxed threshold check or expected rounded value due to floating point calculation - assert abs(report["containment"]["score"] - 0.733) < 0.01 + assert report["containment"]["score"] == 1.0 + + def test_config_from_env_preserves_types(self, monkeypatch): + """Environment values are coerced into config field types.""" + from rules_system_bound.config import Config + monkeypatch.setenv("CONTAINMENT_FUNCTION_WEIGHT", "0.7") + monkeypatch.setenv("LEVEL_NAMES_EMERGE", "false") + + config = Config.from_env() + + assert config.containment_function_weight == 0.7 + assert config.level_names_emerge is False