Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -32,3 +37,4 @@ target-version = "py310"
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
pythonpath = ["src"]
3 changes: 2 additions & 1 deletion src/rules_system_bound/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@

from .cli import main

raise SystemExit(main())
if __name__ == "__main__":
raise SystemExit(main())
189 changes: 116 additions & 73 deletions src/rules_system_bound/cli.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,106 @@
"""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
from .container import Container, ContainmentRules
Comment on lines +10 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 | Confidence: High

The new cli.py imports __version__ from the parent package (from . import __version__). However, __init__.py does not define __version__. Hatchling does not automatically inject it for source imports, so ImportError is raised at import time. This blocks the entire CLI and any imports of cli.py. Fix by defining __version__ in __init__.py or using importlib.metadata.

Code Suggestion:

__version__ = "0.1.0"

Evidence: path:src/rules_system_bound/init.py

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": [
{
Expand All @@ -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": {
Expand All @@ -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}")
Comment on lines +195 to +199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The parser.error call already exits, so the subsequent return 2 is dead code and the explicit unknown-command branch may never be hit in normal CLI use.

Given that argparse already treats unknown subcommands as invalid choice before reaching this point, this branch only matters if main() is called programmatically with a custom Namespace. If that’s not a supported scenario, you can remove the branch entirely. If it is, consider replacing parser.error(...) with a direct print(..., file=sys.stderr) plus return 2 to make the behavior explicit and avoid additional SystemExit handling.

return 2
Comment on lines +195 to +200

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since argparse subparsers restrict the command choices to only the registered subparsers, args.command is guaranteed to be either "version", "audit", "demo", or None. Since "version" and "audit" are already handled and return early, any execution reaching this point is guaranteed to be "demo" or None. We can simplify this block and remove the redundant check and unreachable code.

    _print_report(build_demo_report(), getattr(args, "json", False))
    return 0

31 changes: 18 additions & 13 deletions src/rules_system_bound/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@
}


def _coerce_env_value(value: str, default: Any) -> Any:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 | Confidence: High

When an environment variable is present but cannot be coerced to the expected type, _coerce_env_value raises a bare ValueError. This propagates uncaught out of Config.from_env(), causing a non‑descriptive stack trace. Consider catching the exception and re‑raising with a more informative message, or falling back to the default with a warning.

Suggested change
def _coerce_env_value(value: str, default: Any) -> Any:
def _coerce_env_value(key: str, value: str, default: Any) -> Any:
try:
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
except ValueError as exc:
raise ConfigError(
f"Environment variable '{key.upper()}' = {value!r} "
f"cannot be coerced to type {type(default).__name__}. "
f"Expected a {type(default).__name__} value."
) from exc

Evidence: method:Config.from_env

"""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."""
Expand Down Expand Up @@ -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)
Comment on lines +76 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When loading configuration from environment variables, empty or whitespace-only values (e.g., KEY=) can cause _coerce_env_value to raise a ValueError (for example, when trying to parse an empty string as a float or boolean). Gracefully falling back to the default value when the environment variable is empty or whitespace-only improves robustness, especially in containerized or CI/CD environments where variables are often defined but left empty.

Suggested change
value = os.getenv(env_key)
config[key] = default if value is None else _coerce_env_value(value, default)
value = os.getenv(env_key)
if value is None or value.strip() == "":
config[key] = default
else:
config[key] = _coerce_env_value(value, default)

Comment on lines +76 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 | Confidence: Medium

The _coerce_env_value function currently receives only the string value and the default; it does not receive the environment variable name. When coercion fails, the error message cannot mention which variable caused the problem. Pass the key as well (as suggested in the previous finding) and add a test for the error path.

Code Suggestion:

def test_config_env_coercion_failure_raises_meaningful_error(monkeypatch):
    monkeypatch.setenv("LEVEL_NAMES_EMERGE", "not_a_bool")
    with pytest.raises(ValueError, match="LEVEL_NAMES_EMERGE"):
        Config.from_env()

return cls(**config)

@classmethod
Expand Down
48 changes: 48 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading