From 1b5ed9c13d20bbee646aaac0bef91fa77ab9f1bb Mon Sep 17 00:00:00 2001 From: BELLO SHEHU <1739677116@qq.com> Date: Mon, 27 Apr 2026 12:29:16 +0000 Subject: [PATCH] v0.9: runtime/ scaffold + lifectl CLI + pyproject + scaffold tests (#120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lay down the empty runtime/ Python package, the lifectl CLI entrypoint, and the pyproject.toml that declares the dlrs-runtime package + console script. No assembly logic ships in this PR — Stages 1-5 land in sub-issues #121-#126. - runtime/__init__.py exports __version__='0.9.0.dev0' and LIFE_RUNTIME_PROTOCOL_VERSION='0.1.1' - runtime/{verify,resolve,assemble,run,guard,providers,audit}/ empty sub-packages with one-line docstrings naming the owning sub-issue - runtime/cli/lifectl.py: 'version' subcommand fully working; 'info' / 'run' parse args + exit non-zero with 'not yet implemented' message that points at #121 / #121-#126 - pyproject.toml: name='dlrs-runtime', version='0.9.0.dev0', Python>=3.10, deps jsonschema+pyyaml, exports lifectl console script - tools/test_runtime_scaffold.py: 8 sanity-test cases (import, version, stub exit codes, --help, pyproject parse + script entry, sub-package presence) - .github/workflows/validate.yml: new runtime-scaffold CI job parallel to pipelines (matrix py3.11+3.12), pip install -e ., run tests, confirm lifectl on PATH + scaffold stubs exit non-zero - CHANGELOG.md: v0.9 epic entry + sub-issue #120 'Added' details - .gitignore: add *.egg-info/ + build/ + dist/ Closes part of #119; closes #120. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/validate.yml | 41 ++++++++ .gitignore | 3 + CHANGELOG.md | 72 +++++++++++++ pyproject.toml | 47 +++++++++ runtime/README.md | 52 ++++++++++ runtime/__init__.py | 36 +++++++ runtime/assemble/__init__.py | 1 + runtime/audit/__init__.py | 4 + runtime/audit/emitter.py | 17 +++ runtime/cli/__init__.py | 1 + runtime/cli/lifectl.py | 99 ++++++++++++++++++ runtime/guard/__init__.py | 1 + runtime/providers/__init__.py | 4 + runtime/resolve/__init__.py | 1 + runtime/run/__init__.py | 1 + runtime/verify/__init__.py | 1 + tools/test_runtime_scaffold.py | 182 +++++++++++++++++++++++++++++++++ 17 files changed, 563 insertions(+) create mode 100644 pyproject.toml create mode 100644 runtime/README.md create mode 100644 runtime/__init__.py create mode 100644 runtime/assemble/__init__.py create mode 100644 runtime/audit/__init__.py create mode 100644 runtime/audit/emitter.py create mode 100644 runtime/cli/__init__.py create mode 100644 runtime/cli/lifectl.py create mode 100644 runtime/guard/__init__.py create mode 100644 runtime/providers/__init__.py create mode 100644 runtime/resolve/__init__.py create mode 100644 runtime/run/__init__.py create mode 100644 runtime/verify/__init__.py create mode 100644 tools/test_runtime_scaffold.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 24a87e9..8604711 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -92,6 +92,47 @@ jobs: - name: Run pipeline test suite (per-pipeline + v0.6 cross-cutting) run: python tools/test_pipelines.py + runtime-scaffold: + name: v0.9 runtime scaffold (sub-issue #120) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install runtime package (editable) + run: | + python -m pip install --upgrade pip + python -m pip install -e . + + - name: Run runtime-scaffold test suite + run: python tools/test_runtime_scaffold.py + + - name: Confirm `lifectl` console script installed + run: | + lifectl version + # exit non-zero on info / run is expected (scaffold-only stubs) + set +e + lifectl info pretend.life + info_rc=$? + lifectl run pretend.life + run_rc=$? + set -e + if [ "$info_rc" -eq 0 ] || [ "$run_rc" -eq 0 ]; then + echo "ERROR: lifectl info/run unexpectedly succeeded in scaffold-only build" + exit 1 + fi + echo "scaffold stubs exit non-zero as expected (info=$info_rc, run=$run_rc)" + docs: name: Lint docs (markdownlint + linkcheck) runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 28b2152..8f93705 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ __pycache__/ *.pyc .venv/ .env +*.egg-info/ +build/ +dist/ # Never commit raw high-risk personal data *.mp4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 801a3d0..779a1a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,78 @@ All notable changes to the DLRS project will be documented in this file. +## v0.9 — Reference Runtime Implementation (in progress) + +**Status**: Epic +[#119](https://github.com/Digital-Life-Repository-Standard/DLRS/issues/119) +open. Goal: ship the reference runtime for `life-runtime v0.1.1` +(v0.7 §1–10 + v0.8 Part B 5-stage assembly), single `.life` only. +Multi-`.life`, `.world` and DLRS Extension Architecture remain v0.10+. + +Sub-issues: +[#120](https://github.com/Digital-Life-Repository-Standard/DLRS/issues/120) +scaffold, +[#121](https://github.com/Digital-Life-Repository-Standard/DLRS/issues/121) +Stage 1 Verify, +[#122](https://github.com/Digital-Life-Repository-Standard/DLRS/issues/122) +Stage 2 Resolve, +[#123](https://github.com/Digital-Life-Repository-Standard/DLRS/issues/123) +Stage 3 Assemble, +[#124](https://github.com/Digital-Life-Repository-Standard/DLRS/issues/124) +Stage 4 Run, +[#125](https://github.com/Digital-Life-Repository-Standard/DLRS/issues/125) +Stage 5 Guard, +[#126](https://github.com/Digital-Life-Repository-Standard/DLRS/issues/126) +echo Provider + e2e conformance harness + Quickstart docs. + +### Added (sub-issue #120 — scaffold) + +- `runtime/` Python package (`runtime/__init__.py` exporting `__version__ + = "0.9.0.dev0"` and `LIFE_RUNTIME_PROTOCOL_VERSION = "0.1.1"`) with + empty sub-packages laid out for the 5 assembly stages: `verify/`, + `resolve/`, `assemble/`, `run/`, `guard/`, plus `providers/` (built-in + Provider implementations) and `audit/` (runtime-side hash-chain + emitter). Each empty stage carries a one-line docstring naming the + sub-issue that populates it. +- `runtime/cli/lifectl.py` — `lifectl` CLI entrypoint with three + sub-commands. `lifectl version` prints `lifectl 0.9.0.dev0 + (life-runtime v0.1.1)` and exits 0. `lifectl info ` and + `lifectl run ` parse their arguments but exit non-zero with a + "not yet implemented in this sub-issue" message that points the + reader at the right follow-up sub-issue (#121 / #121-#126). +- `pyproject.toml` at repo root — declares `dlrs-runtime` package + (`name = "dlrs-runtime"`, `version = "0.9.0.dev0"`, `requires-python + >= 3.10`, deps `jsonschema` + `pyyaml`) and exports the `lifectl` + console script via `[project.scripts]`. Setuptools is told to + package only `runtime*` so the existing `tools/` and `pipelines/` + trees stay out of the wheel. +- `runtime/audit/emitter.py` — `RuntimeAuditEmitter` stub class that + raises `NotImplementedError` referencing sub-issue #125 (the full + v0.4 hash-chain emitter ships there). +- `runtime/README.md` — package-level overview pointing at the runtime + spec and naming each sub-package's owning sub-issue. +- `tools/test_runtime_scaffold.py` — eight sanity-test cases covering + package import, `Runtime` stub class, `lifectl version` output, the + scaffold-only stub exits, `lifectl --help` listing all three + sub-commands, the parseability of `pyproject.toml` (asserting the + `lifectl` console-script entry), and that all eight `runtime/` + sub-packages exist. +- `.github/workflows/validate.yml` — new `runtime-scaffold` CI job + parallel to the existing `pipelines` job, matrix Python 3.11 + 3.12. + Installs `dlrs-runtime` editable, runs the scaffold test driver, and + asserts both that the `lifectl` console-script is on `PATH` (`lifectl + version` succeeds) and that `lifectl info` / `lifectl run` exit + non-zero in the scaffold-only build. + +### Hard-rule invariants preserved + +This sub-issue ships no execution code, so the v0.7 + v0.8 hard-rule +invariants (D1=C in-life sandbox / D2=B `bundled_in_life` Provider +refusal / D5=mixed hosted-API AND-gate / D6=fail-close Stage gating) +are upheld trivially: the scaffold cannot violate them because none of +the gates run yet. Sub-issues #121–#126 reinstate each invariant as +they implement the corresponding Stage. + ## v0.8-asset-architecture (2026-04-26) **Status**: Released. v0.8 closes the four asset-architecture gaps left diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..343e4f6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "dlrs-runtime" +version = "0.9.0.dev0" +description = "DLRS reference runtime — `life-runtime v0.1.1` reference implementation." +readme = "runtime/README.md" +requires-python = ">=3.10" +license = { file = "LICENSE" } +authors = [ + { name = "DLRS contributors" }, +] +keywords = ["dlrs", "life", "digital-life", "runtime"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "jsonschema>=4.0.0", + "pyyaml>=6.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", +] + +[project.scripts] +lifectl = "runtime.cli.lifectl:main" + +[project.urls] +Homepage = "https://github.com/Digital-Life-Repository-Standard/DLRS" +Repository = "https://github.com/Digital-Life-Repository-Standard/DLRS" +Issues = "https://github.com/Digital-Life-Repository-Standard/DLRS/issues" +Spec = "https://github.com/Digital-Life-Repository-Standard/DLRS/blob/master/docs/LIFE_RUNTIME_STANDARD.md" + +[tool.setuptools.packages.find] +include = ["runtime*"] +exclude = ["tools*", "pipelines*", "examples*", "tests*"] diff --git a/runtime/README.md b/runtime/README.md new file mode 100644 index 0000000..b3cb9b1 --- /dev/null +++ b/runtime/README.md @@ -0,0 +1,52 @@ +# `runtime/` — DLRS reference runtime + +Reference implementation of the `life-runtime v0.1.1` protocol defined in +[`docs/LIFE_RUNTIME_STANDARD.md`](../docs/LIFE_RUNTIME_STANDARD.md). + +Tracking epic: [#119](https://github.com/Digital-Life-Repository-Standard/DLRS/issues/119) +(v0.9 — Reference Runtime Implementation). + +## Layout + +``` +runtime/ +├── __init__.py exports __version__ + Runtime stub +├── cli/lifectl.py lifectl CLI entrypoint +├── verify/ Stage 1 — sub-issue #121 +├── resolve/ Stage 2 — sub-issue #122 +├── assemble/ Stage 3 — sub-issue #123 +├── run/ Stage 4 — sub-issue #124 +├── guard/ Stage 5 — sub-issue #125 +├── providers/ built-in echo Provider — sub-issue #126 +└── audit/ runtime-side hash-chain emitter — sub-issue #125 +``` + +## Quickstart (post-v0.9) + +``` +pip install -e . # from repo root +lifectl version # confirm install +lifectl run examples/minimal-life-package/out/*.life +``` + +Until v0.9 sub-issues #121-#126 land, `lifectl info` and `lifectl run` exit +with a "not yet implemented in this sub-issue" message — only `lifectl version` +is functional. + +## Why a separate Python package? + +The repo's existing `tools/` directory hosts authoring + validation tooling +(builders, schema linters, pipeline drivers). The runtime is a different +artifact: it loads a finished `.life`, not authors one. Separating the two +keeps the dependency graph clean — `tools/` continues to work with +`tools/requirements.txt`, while `runtime/` is installable via +`pyproject.toml` (`pip install -e .`). + +## Spec conformance + +`life-runtime v0.1.1` includes everything from v0.1 (load sequence, mount +semantics, runtime obligations, termination) plus v0.8 Part B 5-stage +assembly + Provider Registry + graded sandbox + hosted-API AND-gate. +The `runtime/` package implements **the full v0.1.1 spec** for **single +`.life`** sessions only — multi-`.life` ensemble + `.world` + plugins are +v0.10+ work. diff --git a/runtime/__init__.py b/runtime/__init__.py new file mode 100644 index 0000000..da2a411 --- /dev/null +++ b/runtime/__init__.py @@ -0,0 +1,36 @@ +"""DLRS reference runtime — `life-runtime v0.1.1` reference implementation. + +This package implements the protocol defined in +``docs/LIFE_RUNTIME_STANDARD.md`` (v0.7 §1-10 + v0.8 Part B 5-stage assembly). + +Public surface today (v0.9 sub-issue #120 — scaffold only): + +- ``__version__`` — runtime package version (``0.9.0.dev0``). +- ``LIFE_RUNTIME_PROTOCOL_VERSION`` — declared life-runtime spec version. +- ``Runtime`` — placeholder class; concrete assembly stages land in sub-issues + #121-#126. + +The ``runtime.cli.lifectl`` module exposes the ``lifectl`` console script. +""" + +from __future__ import annotations + +__version__ = "0.9.0.dev0" +LIFE_RUNTIME_PROTOCOL_VERSION = "0.1.1" + + +class Runtime: + """Stub Runtime class. Concrete behaviour added in sub-issues #121-#126.""" + + def __init__(self) -> None: + self.version = __version__ + self.protocol = LIFE_RUNTIME_PROTOCOL_VERSION + + def __repr__(self) -> str: + return ( + f"Runtime(version={self.version!r}, " + f"protocol={self.protocol!r}, stages_implemented=[])" + ) + + +__all__ = ["__version__", "LIFE_RUNTIME_PROTOCOL_VERSION", "Runtime"] diff --git a/runtime/assemble/__init__.py b/runtime/assemble/__init__.py new file mode 100644 index 0000000..a3229be --- /dev/null +++ b/runtime/assemble/__init__.py @@ -0,0 +1 @@ +"""Stage 3 — Assemble. Populated in v0.9 sub-issue #123.""" diff --git a/runtime/audit/__init__.py b/runtime/audit/__init__.py new file mode 100644 index 0000000..c7a0555 --- /dev/null +++ b/runtime/audit/__init__.py @@ -0,0 +1,4 @@ +"""Runtime-side audit emitter (v0.4 hash chain). + +Stub in v0.9 sub-issue #120; full implementation lands in #125 (Stage 5 Guard). +""" diff --git a/runtime/audit/emitter.py b/runtime/audit/emitter.py new file mode 100644 index 0000000..64a6b65 --- /dev/null +++ b/runtime/audit/emitter.py @@ -0,0 +1,17 @@ +"""Runtime audit emitter — STUB. + +Sub-issue #120 lays down the module surface; sub-issue #125 (Stage 5 Guard) +populates the full hash-chain implementation that re-uses the v0.4 audit +event vocabulary. +""" + +from __future__ import annotations + + +class RuntimeAuditEmitter: + """Placeholder. Concrete emitter ships with v0.9 sub-issue #125.""" + + def __init__(self, *_args: object, **_kwargs: object) -> None: # noqa: D401 + raise NotImplementedError( + "RuntimeAuditEmitter lands in v0.9 sub-issue #125 (Stage 5 Guard)." + ) diff --git a/runtime/cli/__init__.py b/runtime/cli/__init__.py new file mode 100644 index 0000000..557d142 --- /dev/null +++ b/runtime/cli/__init__.py @@ -0,0 +1 @@ +"""DLRS runtime CLI entrypoints (``lifectl``).""" diff --git a/runtime/cli/lifectl.py b/runtime/cli/lifectl.py new file mode 100644 index 0000000..d5c5606 --- /dev/null +++ b/runtime/cli/lifectl.py @@ -0,0 +1,99 @@ +"""``lifectl`` — DLRS reference runtime CLI. + +v0.9 sub-issue #120 (scaffold). Concrete assembly logic lands in #121-#126. +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from runtime import LIFE_RUNTIME_PROTOCOL_VERSION, __version__ + +_NOT_IMPLEMENTED_INFO = ( + "lifectl info: not yet implemented (v0.9 sub-issue #121 — Stage 1 Verify)." +) +_NOT_IMPLEMENTED_RUN = ( + "lifectl run: not yet implemented (v0.9 sub-issues #121-#126 — full 5-stage " + "assembly)." +) + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="lifectl", + description=( + "DLRS reference runtime CLI. Mounts a single `.life` archive end-to-end " + "via the 5-stage assembly pipeline (Verify → Resolve → Assemble → Run " + "→ Guard) defined in docs/LIFE_RUNTIME_STANDARD.md." + ), + ) + sub = parser.add_subparsers(dest="command", required=True) + + sub.add_parser("version", help="print runtime version + protocol version and exit") + + info = sub.add_parser( + "info", + help="print a structured verification report for a `.life` archive (Stage 1 only)", + ) + info.add_argument("life_path", type=Path, help="path to a `.life` archive") + + run = sub.add_parser("run", help="mount and run a `.life` archive end-to-end") + run.add_argument("life_path", type=Path, help="path to a `.life` archive") + run.add_argument( + "--once", + action="store_true", + help="read one stdin line, process one turn, exit (test/CI use)", + ) + run.add_argument( + "--no-tty", + action="store_true", + help="non-interactive mode (no readline / no prompt)", + ) + run.add_argument( + "--poll-interval-override", + type=float, + default=None, + metavar="SECONDS", + help=( + "override the Stage 5 watcher poll interval (test-only; spec mandates " + "≥24h in production)" + ), + ) + + return parser + + +def cmd_version() -> int: + print(f"lifectl {__version__} (life-runtime v{LIFE_RUNTIME_PROTOCOL_VERSION})") + return 0 + + +def cmd_info(_args: argparse.Namespace) -> int: + print(_NOT_IMPLEMENTED_INFO, file=sys.stderr) + return 2 + + +def cmd_run(_args: argparse.Namespace) -> int: + print(_NOT_IMPLEMENTED_RUN, file=sys.stderr) + return 2 + + +def main(argv: list[str] | None = None) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + + if args.command == "version": + return cmd_version() + if args.command == "info": + return cmd_info(args) + if args.command == "run": + return cmd_run(args) + + parser.print_help(sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/runtime/guard/__init__.py b/runtime/guard/__init__.py new file mode 100644 index 0000000..78d431e --- /dev/null +++ b/runtime/guard/__init__.py @@ -0,0 +1 @@ +"""Stage 5 — Guard. Populated in v0.9 sub-issue #125.""" diff --git a/runtime/providers/__init__.py b/runtime/providers/__init__.py new file mode 100644 index 0000000..e846bce --- /dev/null +++ b/runtime/providers/__init__.py @@ -0,0 +1,4 @@ +"""Built-in `LifeCapabilityProvider` implementations. + +Populated in v0.9 sub-issue #126 (echo Provider for `text_chat`). +""" diff --git a/runtime/resolve/__init__.py b/runtime/resolve/__init__.py new file mode 100644 index 0000000..e6bab9a --- /dev/null +++ b/runtime/resolve/__init__.py @@ -0,0 +1 @@ +"""Stage 2 — Resolve. Populated in v0.9 sub-issue #122.""" diff --git a/runtime/run/__init__.py b/runtime/run/__init__.py new file mode 100644 index 0000000..147b6aa --- /dev/null +++ b/runtime/run/__init__.py @@ -0,0 +1 @@ +"""Stage 4 — Run. Populated in v0.9 sub-issue #124.""" diff --git a/runtime/verify/__init__.py b/runtime/verify/__init__.py new file mode 100644 index 0000000..d8348a8 --- /dev/null +++ b/runtime/verify/__init__.py @@ -0,0 +1 @@ +"""Stage 1 — Verify. Populated in v0.9 sub-issue #121.""" diff --git a/tools/test_runtime_scaffold.py b/tools/test_runtime_scaffold.py new file mode 100644 index 0000000..06a0e9f --- /dev/null +++ b/tools/test_runtime_scaffold.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Sanity tests for the v0.9 runtime scaffold (sub-issue #120). + +Verifies: + +1. `lifectl version` exits 0 and prints the expected version string. +2. `lifectl info ` exits non-zero with a "not yet implemented" stderr + message. +3. `lifectl run ` exits non-zero with a "not yet implemented" stderr + message. +4. `import runtime; from runtime import Runtime, __version__` works. +5. `pyproject.toml` parses + declares the `lifectl` console script. + +Stages 1-5 (#121-#126) replace the "not yet implemented" stubs with the +full assembly pipeline. +""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +def _python() -> str: + return sys.executable + + +def test_runtime_module_importable() -> None: + proc = subprocess.run( + [_python(), "-c", "import runtime; print(runtime.__version__)"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + assert proc.returncode == 0, proc.stderr + assert proc.stdout.strip() == "0.9.0.dev0" + + +def test_runtime_class_present() -> None: + proc = subprocess.run( + [ + _python(), + "-c", + "from runtime import Runtime; r = Runtime(); print(r.protocol)", + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + assert proc.returncode == 0, proc.stderr + assert proc.stdout.strip() == "0.1.1" + + +def test_lifectl_version_via_module() -> None: + proc = subprocess.run( + [_python(), "-m", "runtime.cli.lifectl", "version"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + assert proc.returncode == 0, proc.stderr + out = proc.stdout.strip() + assert out.startswith("lifectl 0.9.0.dev0"), out + assert "life-runtime v0.1.1" in out, out + + +def test_lifectl_info_not_implemented() -> None: + proc = subprocess.run( + [_python(), "-m", "runtime.cli.lifectl", "info", "pretend.life"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + assert proc.returncode != 0 + assert "not yet implemented" in proc.stderr + assert "#121" in proc.stderr # points the reader at the right sub-issue + + +def test_lifectl_run_not_implemented() -> None: + proc = subprocess.run( + [_python(), "-m", "runtime.cli.lifectl", "run", "pretend.life"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + assert proc.returncode != 0 + assert "not yet implemented" in proc.stderr + assert "#121-#126" in proc.stderr or "121" in proc.stderr + + +def test_lifectl_help_lists_three_commands() -> None: + proc = subprocess.run( + [_python(), "-m", "runtime.cli.lifectl", "--help"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + assert proc.returncode == 0, proc.stderr + out = proc.stdout + assert "version" in out + assert "info" in out + assert "run" in out + + +def test_pyproject_parses_and_declares_lifectl_script() -> None: + pyproject = REPO_ROOT / "pyproject.toml" + assert pyproject.is_file(), "pyproject.toml missing at repo root" + + if sys.version_info >= (3, 11): + import tomllib + else: # pragma: no cover - py310 fallback + import tomli as tomllib # type: ignore[no-redef] + + data = tomllib.loads(pyproject.read_text()) + assert data["project"]["name"] == "dlrs-runtime" + assert data["project"]["version"] == "0.9.0.dev0" + scripts = data["project"]["scripts"] + assert scripts.get("lifectl") == "runtime.cli.lifectl:main", ( + "expected lifectl script entry pointing at runtime.cli.lifectl:main" + ) + + +def test_runtime_subpackages_present() -> None: + runtime_dir = REPO_ROOT / "runtime" + expected = { + "cli", + "verify", + "resolve", + "assemble", + "run", + "guard", + "providers", + "audit", + } + actual = { + p.name + for p in runtime_dir.iterdir() + if p.is_dir() and not p.name.startswith("__") + } + assert expected.issubset(actual), ( + f"runtime/ missing sub-packages: {expected - actual}" + ) + + +def main() -> int: + tests = [ + test_runtime_module_importable, + test_runtime_class_present, + test_lifectl_version_via_module, + test_lifectl_info_not_implemented, + test_lifectl_run_not_implemented, + test_lifectl_help_lists_three_commands, + test_pyproject_parses_and_declares_lifectl_script, + test_runtime_subpackages_present, + ] + failures: list[str] = [] + for test in tests: + name = test.__name__ + try: + test() + except AssertionError as exc: + failures.append(f"FAIL {name}: {exc}") + except Exception as exc: # pragma: no cover - surfacing unexpected errors + failures.append(f"ERROR {name}: {type(exc).__name__}: {exc}") + else: + print(f"ok {name}") + + if failures: + for line in failures: + print(line, file=sys.stderr) + print(f"\n{len(failures)} of {len(tests)} runtime-scaffold tests failed.", file=sys.stderr) + return 1 + print(f"\n{len(tests)} runtime-scaffold tests passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())