From 8fdf06cb2b35ffafb36d55d038e220e18a820d08 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:22:02 +0300 Subject: [PATCH 01/79] docs: define Lotus Family conformance v0.1 --- standards/lotus-family/README.md | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 standards/lotus-family/README.md diff --git a/standards/lotus-family/README.md b/standards/lotus-family/README.md new file mode 100644 index 00000000..672f567c --- /dev/null +++ b/standards/lotus-family/README.md @@ -0,0 +1,51 @@ +# Lotus Family Conformance v0.1 + +A bounded, machine-readable audit surface for checking the shared Lotus contract +across Pythia, CML, and LS. + +## Outcomes + +- `PASS` — every configured invariant was verified for the supplied repository + snapshot and exact commit SHA. +- `DRIFT` — the snapshot is available, but a required contract term, regression + test, or CI discovery rule is missing. +- `UNKNOWN` — the repository snapshot, exact ref, commit SHA, or manifest could + not be verified. `UNKNOWN` is never promoted to `PASS`. + +## Boundary + +The auditor is read-only and `audit_only`. Its result does not grant ownership, +approval, execution, delivery, deployment, or merge authority. + +It does not fetch repositories, call GitHub, merge pull requests, or deploy +software. An integration must materialize a repository snapshot at the exact +commit it claims to audit. + +## Snapshot layout + +```text +snapshot-root/ + safal207__pythiaLabs/ + safal207__Causal-Memory-Layer/ + safal207__LS/ +``` + +Each directory must contain the files named by +[`manifest/lotus-family-v0.1.json`](manifest/lotus-family-v0.1.json). + +## Run one audit + +```bash +python standards/lotus-family/conformance/lotus_family_auditor.py \ + --manifest standards/lotus-family/manifest/lotus-family-v0.1.json \ + --snapshot-root /path/to/exact-snapshots \ + --repository-id pythia \ + --repository-ref refs/heads/main \ + --commit-sha 0123456789abcdef0123456789abcdef01234567 \ + --output artifacts/lotus-family/pythia.json +``` + +Exit codes are `0` for `PASS`, `2` for `DRIFT`, and `3` for `UNKNOWN`. + +The evidence artifact records the exact repository identity, ref, commit SHA, +check outcomes, checked file paths, and SHA-256 hashes. From 90b806b094bb63c15e18466afd3f05ef20833f49 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:22:14 +0300 Subject: [PATCH 02/79] feat: add Lotus Family conformance manifest --- .../manifest/lotus-family-v0.1.json | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 standards/lotus-family/manifest/lotus-family-v0.1.json diff --git a/standards/lotus-family/manifest/lotus-family-v0.1.json b/standards/lotus-family/manifest/lotus-family-v0.1.json new file mode 100644 index 00000000..6b9e2b26 --- /dev/null +++ b/standards/lotus-family/manifest/lotus-family-v0.1.json @@ -0,0 +1,156 @@ +{ + "schema_version": "pythia.lotus_family_manifest.v0.1", + "authority": "audit_only", + "outcomes": [ + "PASS", + "DRIFT", + "UNKNOWN" + ], + "repositories": [ + { + "id": "pythia", + "repository": "safal207/pythiaLabs", + "snapshot_dir": "safal207__pythiaLabs", + "file_checks": [ + { + "id": "bilingual_contract", + "path": "LOTUS.md", + "contains_all": [ + "Evidence before verdict", + "Judgment without execution", + "Human challengeability", + "Доказательства до вердикта", + "Суждение без исполнения", + "Право человека оспорить" + ] + }, + { + "id": "authority_firewall", + "path": "LOTUS.md", + "contains_all": [ + "has no ownership, credential, execution, delivery, or merge authority", + "не имеет права собственности", + "права на исполнение", + "доставку или merge" + ] + }, + { + "id": "exact_head_and_supersession", + "path": ".github/pull_request_template.md", + "contains_all": [ + "Exact PR head SHA validated", + "Validation was run or rerun after the most recent PR head change", + "Evidence becomes stale" + ] + }, + { + "id": "regression_protection", + "path": "test/lotus_docs_contract_test.exs", + "contains_all": [ + "PR evidence is bound to exact head and changed context", + "English and Russian contracts preserve the no-authority boundary", + "Lotus remains a limitation contract rather than a safety overclaim" + ] + } + ], + "ci_discovery": { + "workflow_paths": [ + ".github/workflows/ci.yml" + ], + "contains_any": [ + "mix test" + ] + } + }, + { + "id": "cml", + "repository": "safal207/Causal-Memory-Layer", + "snapshot_dir": "safal207__Causal-Memory-Layer", + "file_checks": [ + { + "id": "bilingual_contract", + "path": "docs/LOTUS.md", + "contains_all": [ + "Memory without authority", + "Память без власти", + "has no ownership, approval, execution, delivery, or merge authority", + "не имеет права собственности", + "доставки или merge" + ] + }, + { + "id": "exact_head_and_supersession", + "path": ".github/pull_request_template.md", + "contains_all": [ + "Exact PR head SHA validated", + "Validation was run or rerun after the most recent head change", + "Evidence becomes stale" + ] + }, + { + "id": "regression_protection", + "path": "tests/test_lotus_docs_contract.py", + "contains_all": [ + "test_english_and_russian_contracts_keep_the_no_authority_boundary", + "has no ownership, approval, execution, delivery, or merge authority", + "не имеет права собственности" + ] + } + ], + "ci_discovery": { + "workflow_paths": [ + ".github/workflows/ci.yml" + ], + "contains_any": [ + "python -m pytest" + ] + } + }, + { + "id": "ls", + "repository": "safal207/LS", + "snapshot_dir": "safal207__LS", + "file_checks": [ + { + "id": "bilingual_contract", + "path": "LOTUS.md", + "contains_all": [ + "Clarity from complexity", + "Human authorship at the center", + "Ясность из сложности", + "Человек остаётся автором", + "has no ownership, approval, execution, delivery, or merge authority", + "не имеет права собственности", + "доставки или merge" + ] + }, + { + "id": "exact_head_and_supersession", + "path": ".github/pull_request_template.md", + "contains_all": [ + "Exact PR head SHA validated", + "Validation was run or rerun after the most recent PR head change", + "Evidence becomes stale" + ] + }, + { + "id": "regression_protection", + "path": "tests/test_lotus_docs_contract.py", + "contains_all": [ + "test_pr_evidence_is_bound_to_exact_head", + "test_lotus_preserves_human_authority_in_both_languages", + "test_english_and_russian_contracts_keep_the_seven_core_petals" + ] + } + ], + "ci_discovery": { + "workflow_paths": [ + ".github/workflows/ci.yml" + ], + "contains_any": [ + "tests/test_lotus_docs_contract.py" + ] + } + } + ] +} From c838f83401c454a5a722aaaeee183d9794f179ef Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:22:33 +0300 Subject: [PATCH 03/79] feat: add Lotus Family reference auditor --- .../conformance/lotus_family_auditor.py | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 standards/lotus-family/conformance/lotus_family_auditor.py diff --git a/standards/lotus-family/conformance/lotus_family_auditor.py b/standards/lotus-family/conformance/lotus_family_auditor.py new file mode 100644 index 00000000..a7ff7637 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from pathlib import Path +from typing import Any, Mapping + +PASS = "PASS" +DRIFT = "DRIFT" +UNKNOWN = "UNKNOWN" + +_COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") + + +def load_json_object(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"{path} root must be an object") + return value + + +def load_manifest(path: Path) -> dict[str, Any]: + manifest = load_json_object(path) + if manifest.get("schema_version") != "pythia.lotus_family_manifest.v0.1": + raise ValueError("unsupported Lotus Family manifest schema") + if manifest.get("authority") != "audit_only": + raise ValueError("Lotus Family manifest authority must be audit_only") + repositories = manifest.get("repositories") + if not isinstance(repositories, list) or not repositories: + raise ValueError("manifest repositories must be a non-empty list") + return manifest + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _result( + *, + outcome: str, + reason_code: str, + detail: str, + repository: str, + repository_id: str, + repository_ref: str, + commit_sha: str, + manifest_schema: str, + checks: list[dict[str, Any]], + files: list[dict[str, str]], +) -> dict[str, Any]: + return { + "schema_version": "pythia.lotus_family_audit_result.v0.1", + "outcome": outcome, + "reason_code": reason_code, + "detail": detail, + "repository": repository, + "repository_id": repository_id, + "repository_ref": repository_ref, + "commit_sha": commit_sha, + "manifest_schema": manifest_schema, + "checks": checks, + "files": sorted(files, key=lambda row: row["path"]), + "authority": { + "mode": "audit_only", + "grants_ownership": False, + "grants_approval": False, + "grants_execution": False, + "grants_delivery": False, + "grants_merge": False, + }, + } + + +def _repository_config( + manifest: Mapping[str, Any], repository_id: str +) -> Mapping[str, Any] | None: + for row in manifest["repositories"]: + if row.get("id") == repository_id: + return row + return None + + +def _read_file( + root: Path, + relative_path: str, + files: list[dict[str, str]], +) -> tuple[str | None, str | None]: + path = root / relative_path + if not path.is_file(): + return None, f"required file is missing: {relative_path}" + try: + text = path.read_text(encoding="utf-8") + files.append({"path": relative_path, "sha256": _sha256(path)}) + return text, None + except (OSError, UnicodeError) as exc: + return None, f"cannot read {relative_path}: {exc.__class__.__name__}" + + +def audit_repository( + manifest: Mapping[str, Any], + *, + repository_id: str, + snapshot_root: Path, + repository_ref: str, + commit_sha: str, +) -> dict[str, Any]: + manifest_schema = str(manifest.get("schema_version", "")) + config = _repository_config(manifest, repository_id) + if config is None: + return _result( + outcome=UNKNOWN, + reason_code="REPOSITORY_NOT_CONFIGURED", + detail=f"repository adapter is not configured: {repository_id}", + repository="", + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + + repository = str(config["repository"]) + if not repository_ref: + return _result( + outcome=UNKNOWN, + reason_code="REPOSITORY_REF_MISSING", + detail="an exact repository ref is required", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + if not _COMMIT_SHA.fullmatch(commit_sha): + return _result( + outcome=UNKNOWN, + reason_code="COMMIT_SHA_INVALID", + detail="commit_sha must be a lowercase 40-character hexadecimal SHA", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + + repository_root = snapshot_root / str(config["snapshot_dir"]) + if not repository_root.is_dir(): + return _result( + outcome=UNKNOWN, + reason_code="SNAPSHOT_UNAVAILABLE", + detail=f"repository snapshot is unavailable: {config['snapshot_dir']}", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + + checks: list[dict[str, Any]] = [] + files: list[dict[str, str]] = [] + drift: list[str] = [] + + for check in config.get("file_checks", []): + check_id = str(check["id"]) + relative_path = str(check["path"]) + text, error = _read_file(repository_root, relative_path, files) + if error is not None: + checks.append( + { + "check_id": check_id, + "outcome": DRIFT, + "path": relative_path, + "missing_terms": [], + "detail": error, + } + ) + drift.append(check_id) + continue + + missing_terms = [ + term for term in check.get("contains_all", []) if term not in text + ] + outcome = PASS if not missing_terms else DRIFT + checks.append( + { + "check_id": check_id, + "outcome": outcome, + "path": relative_path, + "missing_terms": missing_terms, + "detail": ( + "all required terms are present" + if outcome == PASS + else "required terms are missing" + ), + } + ) + if outcome == DRIFT: + drift.append(check_id) + + discovery = config.get("ci_discovery") + if discovery: + workflow_paths = [str(path) for path in discovery["workflow_paths"]] + workflow_texts: list[str] = [] + read_errors: list[str] = [] + for relative_path in workflow_paths: + text, error = _read_file(repository_root, relative_path, files) + if error is not None: + read_errors.append(error) + else: + workflow_texts.append(text) + + combined = "\n".join(workflow_texts) + patterns = [str(pattern) for pattern in discovery["contains_any"]] + discovered = any(pattern in combined for pattern in patterns) + if read_errors and not workflow_texts: + outcome = DRIFT + detail = "; ".join(read_errors) + drift.append("ci_discovery") + elif discovered: + outcome = PASS + detail = "contract regression test is discovered by CI" + else: + outcome = DRIFT + detail = "no configured CI discovery pattern was found" + drift.append("ci_discovery") + checks.append( + { + "check_id": "ci_discovery", + "outcome": outcome, + "paths": workflow_paths, + "matched_patterns": [ + pattern for pattern in patterns if pattern in combined + ], + "detail": detail, + } + ) + + if drift: + outcome = DRIFT + reason_code = "LOTUS_CONTRACT_DRIFT" + detail = "non-conforming checks: " + ", ".join(sorted(set(drift))) + else: + outcome = PASS + reason_code = "LOTUS_CONTRACT_CONFORMANT" + detail = "all configured Lotus Family invariants passed" + + unique_files = {(row["path"], row["sha256"]): row for row in files} + return _result( + outcome=outcome, + reason_code=reason_code, + detail=detail, + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=checks, + files=list(unique_files.values()), + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Audit one exact repository snapshot against the Lotus Family manifest." + ) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--snapshot-root", type=Path, required=True) + parser.add_argument("--repository-id", required=True) + parser.add_argument("--repository-ref", required=True) + parser.add_argument("--commit-sha", required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args(argv) + + try: + manifest = load_manifest(args.manifest) + result = audit_repository( + manifest, + repository_id=args.repository_id, + snapshot_root=args.snapshot_root, + repository_ref=args.repository_ref, + commit_sha=args.commit_sha, + ) + except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: + result = { + "schema_version": "pythia.lotus_family_audit_result.v0.1", + "outcome": UNKNOWN, + "reason_code": "MANIFEST_INVALID", + "detail": str(exc), + "authority": { + "mode": "audit_only", + "grants_ownership": False, + "grants_approval": False, + "grants_execution": False, + "grants_delivery": False, + "grants_merge": False, + }, + } + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(result, ensure_ascii=False, sort_keys=True)) + return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(result["outcome"], 3) + + +if __name__ == "__main__": + raise SystemExit(main()) From 99303c5068bd1fd9a17cc1c73fcaa961909f1692 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:22:47 +0300 Subject: [PATCH 04/79] test: add Lotus Family auditor regressions --- .../conformance/test_lotus_family_auditor.py | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 standards/lotus-family/conformance/test_lotus_family_auditor.py diff --git a/standards/lotus-family/conformance/test_lotus_family_auditor.py b/standards/lotus-family/conformance/test_lotus_family_auditor.py new file mode 100644 index 00000000..96af48c5 --- /dev/null +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import copy +import tempfile +import unittest +from pathlib import Path + +from lotus_family_auditor import DRIFT, PASS, UNKNOWN, audit_repository, load_manifest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +MANIFEST_PATH = ROOT / "manifest" / "lotus-family-v0.1.json" +SHA = "a" * 40 + + +def _write(root: Path, relative_path: str, content: str) -> None: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _materialize_repository(snapshot_root: Path, config: dict) -> Path: + repository_root = snapshot_root / config["snapshot_dir"] + terms_by_path: dict[str, list[str]] = {} + for check in config["file_checks"]: + terms_by_path.setdefault(check["path"], []).extend(check["contains_all"]) + for relative_path, terms in terms_by_path.items(): + _write(repository_root, relative_path, "\n".join(dict.fromkeys(terms)) + "\n") + discovery = config["ci_discovery"] + _write( + repository_root, + discovery["workflow_paths"][0], + "\n".join(discovery["contains_any"]) + "\n", + ) + return repository_root + + +class LotusFamilyAuditorTest(unittest.TestCase): + def setUp(self) -> None: + self.manifest = load_manifest(MANIFEST_PATH) + + def config(self, repository_id: str) -> dict: + return next( + row for row in self.manifest["repositories"] if row["id"] == repository_id + ) + + def audit(self, repository_id: str, snapshot_root: Path): + return audit_repository( + self.manifest, + repository_id=repository_id, + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + def test_manifest_has_three_repository_adapters_and_audit_only_authority(self): + self.assertEqual(self.manifest["authority"], "audit_only") + self.assertEqual( + {row["id"] for row in self.manifest["repositories"]}, + {"pythia", "cml", "ls"}, + ) + + def test_valid_pythia_snapshot_passes_with_exact_identity_and_hashes(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("pythia")) + + result = self.audit("pythia", snapshot_root) + + self.assertEqual( + (result["outcome"], result["reason_code"]), + (PASS, "LOTUS_CONTRACT_CONFORMANT"), + ) + self.assertEqual(result["repository"], "safal207/pythiaLabs") + self.assertEqual(result["repository_ref"], "refs/heads/main") + self.assertEqual(result["commit_sha"], SHA) + self.assertTrue(result["files"]) + for row in result["files"]: + self.assertRegex(row["sha256"], r"^[0-9a-f]{64}$") + self.assertFalse(result["authority"]["grants_merge"]) + + def test_bilingual_authority_deletion_is_drift(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + config = copy.deepcopy(self.config("cml")) + repository_root = _materialize_repository(snapshot_root, config) + contract = repository_root / "docs/LOTUS.md" + contract.write_text( + contract.read_text(encoding="utf-8").replace( + "не имеет права собственности", "" + ), + encoding="utf-8", + ) + + result = self.audit("cml", snapshot_root) + + self.assertEqual(result["outcome"], DRIFT) + self.assertEqual(result["reason_code"], "LOTUS_CONTRACT_DRIFT") + authority_check = next( + row + for row in result["checks"] + if row["check_id"] == "bilingual_contract" + ) + self.assertIn( + "не имеет права собственности", + authority_check["missing_terms"], + ) + + def test_contract_test_not_discovered_by_ci_is_drift(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + config = self.config("ls") + repository_root = _materialize_repository(snapshot_root, config) + workflow = repository_root / ".github/workflows/ci.yml" + workflow.write_text("python -m pytest tests/test_other.py\n", encoding="utf-8") + + result = self.audit("ls", snapshot_root) + + self.assertEqual(result["outcome"], DRIFT) + discovery = next( + row for row in result["checks"] if row["check_id"] == "ci_discovery" + ) + self.assertEqual(discovery["outcome"], DRIFT) + self.assertEqual(discovery["matched_patterns"], []) + + def test_missing_repository_snapshot_is_unknown_not_pass(self): + with tempfile.TemporaryDirectory() as directory: + result = self.audit("pythia", Path(directory)) + + self.assertEqual( + (result["outcome"], result["reason_code"]), + (UNKNOWN, "SNAPSHOT_UNAVAILABLE"), + ) + + def test_invalid_commit_sha_is_unknown_not_pass(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("pythia")) + + result = audit_repository( + self.manifest, + repository_id="pythia", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha="main", + ) + + self.assertEqual( + (result["outcome"], result["reason_code"]), + (UNKNOWN, "COMMIT_SHA_INVALID"), + ) + + def test_file_hash_changes_when_checked_content_changes(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_repository( + snapshot_root, self.config("pythia") + ) + first = self.audit("pythia", snapshot_root) + contract = repository_root / "LOTUS.md" + contract.write_text( + contract.read_text(encoding="utf-8") + "extra evidence\n", + encoding="utf-8", + ) + second = self.audit("pythia", snapshot_root) + + first_hash = next( + row["sha256"] for row in first["files"] if row["path"] == "LOTUS.md" + ) + second_hash = next( + row["sha256"] for row in second["files"] if row["path"] == "LOTUS.md" + ) + self.assertNotEqual(first_hash, second_hash) + + +if __name__ == "__main__": + unittest.main() From d59ab1ae49a5a8bbeba13e9d40918e08e16d3462 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:22:53 +0300 Subject: [PATCH 05/79] ci: run Lotus Family conformance suite --- .../workflows/lotus-family-conformance.yml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/lotus-family-conformance.yml diff --git a/.github/workflows/lotus-family-conformance.yml b/.github/workflows/lotus-family-conformance.yml new file mode 100644 index 00000000..69b28dd1 --- /dev/null +++ b/.github/workflows/lotus-family-conformance.yml @@ -0,0 +1,36 @@ +name: Lotus Family conformance + +on: + pull_request: + paths: + - "standards/lotus-family/**" + - ".github/workflows/lotus-family-conformance.yml" + push: + paths: + - "standards/lotus-family/**" + - ".github/workflows/lotus-family-conformance.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + conformance: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Run Lotus Family conformance suite + run: | + python -m unittest discover \ + -s standards/lotus-family/conformance \ + -p 'test_*.py' \ + -v From e2d48b5d1361a9adcf0808fca4f9dc35a9ccccdc Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:22:59 +0300 Subject: [PATCH 06/79] chore: mark Lotus Family conformance package --- standards/lotus-family/conformance/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 standards/lotus-family/conformance/__init__.py diff --git a/standards/lotus-family/conformance/__init__.py b/standards/lotus-family/conformance/__init__.py new file mode 100644 index 00000000..e69de29b From f4b1e1425325e735420180280484cdfe6e8c5edf Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:23:04 +0300 Subject: [PATCH 07/79] chore: preserve Lotus Family conformance directory --- standards/lotus-family/conformance/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 standards/lotus-family/conformance/.gitkeep diff --git a/standards/lotus-family/conformance/.gitkeep b/standards/lotus-family/conformance/.gitkeep new file mode 100644 index 00000000..e69de29b From 7d4496371d4ae42580a40e398921ead95c5366aa Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:23:11 +0300 Subject: [PATCH 08/79] chore: remove redundant placeholder --- standards/lotus-family/conformance/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 standards/lotus-family/conformance/.gitkeep diff --git a/standards/lotus-family/conformance/.gitkeep b/standards/lotus-family/conformance/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 958f5fc830ac17e64abc1a40496b1f6e70cde689 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:23:18 +0300 Subject: [PATCH 09/79] chore: record Lotus Family version --- standards/lotus-family/VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 standards/lotus-family/VERSION diff --git a/standards/lotus-family/VERSION b/standards/lotus-family/VERSION new file mode 100644 index 00000000..49d59571 --- /dev/null +++ b/standards/lotus-family/VERSION @@ -0,0 +1 @@ +0.1 From 4bb0401f835449e9b1fb43ffad63bdadde36bacd Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:23:27 +0300 Subject: [PATCH 10/79] chore: ignore local Lotus Family artifacts --- standards/lotus-family/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 standards/lotus-family/.gitignore diff --git a/standards/lotus-family/.gitignore b/standards/lotus-family/.gitignore new file mode 100644 index 00000000..70cfce69 --- /dev/null +++ b/standards/lotus-family/.gitignore @@ -0,0 +1,2 @@ +artifacts/ +__pycache__/ From 31344af6adc6ce9bfd2b69165771fe2f32b94118 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:23:32 +0300 Subject: [PATCH 11/79] docs: add Lotus Family authority notice --- standards/lotus-family/NOTICE.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 standards/lotus-family/NOTICE.md diff --git a/standards/lotus-family/NOTICE.md b/standards/lotus-family/NOTICE.md new file mode 100644 index 00000000..ecd6ebe9 --- /dev/null +++ b/standards/lotus-family/NOTICE.md @@ -0,0 +1,4 @@ +# Authority notice + +Lotus Family audit output is advisory evidence only. It does not grant ownership, +approval, execution, delivery, deployment, or merge authority. From 77bf754eb9dcb2eb487fadee7c8f07fe09c36a7e Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:23:37 +0300 Subject: [PATCH 12/79] docs: describe Lotus Family conformance implementation --- standards/lotus-family/conformance/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 standards/lotus-family/conformance/README.md diff --git a/standards/lotus-family/conformance/README.md b/standards/lotus-family/conformance/README.md new file mode 100644 index 00000000..003a392f --- /dev/null +++ b/standards/lotus-family/conformance/README.md @@ -0,0 +1,5 @@ +# Conformance implementation + +`lotus_family_auditor.py` is the deterministic, standard-library reference +auditor. `test_lotus_family_auditor.py` contains the executable drift and +unknown-state regressions. From a1ef856c479d8ee41cf96a529e45add52e28b80d Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:23:45 +0300 Subject: [PATCH 13/79] docs: add Lotus Family audit decision codes --- standards/lotus-family/DECISION-CODES.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 standards/lotus-family/DECISION-CODES.md diff --git a/standards/lotus-family/DECISION-CODES.md b/standards/lotus-family/DECISION-CODES.md new file mode 100644 index 00000000..68e9f940 --- /dev/null +++ b/standards/lotus-family/DECISION-CODES.md @@ -0,0 +1,13 @@ +# Lotus Family audit result codes + +| Outcome | Reason code | Meaning | +|---|---|---| +| `PASS` | `LOTUS_CONTRACT_CONFORMANT` | Every configured invariant passed for the exact snapshot. | +| `DRIFT` | `LOTUS_CONTRACT_DRIFT` | A required contract, test, or CI discovery invariant is missing. | +| `UNKNOWN` | `SNAPSHOT_UNAVAILABLE` | The repository snapshot is unavailable. | +| `UNKNOWN` | `COMMIT_SHA_INVALID` | The supplied commit identity is not an exact 40-character SHA. | +| `UNKNOWN` | `REPOSITORY_REF_MISSING` | The exact repository ref was not supplied. | +| `UNKNOWN` | `REPOSITORY_NOT_CONFIGURED` | No manifest adapter exists for the repository ID. | +| `UNKNOWN` | `MANIFEST_INVALID` | The manifest cannot be parsed or violates the audit-only boundary. | + +Consumers must branch on `outcome` and `reason_code`, not on human-readable detail. From d28a5658ee9e528fbf5a70317aba3b3c4df300f1 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:23:51 +0300 Subject: [PATCH 14/79] docs: add Lotus Family auditor roadmap --- standards/lotus-family/ROADMAP.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 standards/lotus-family/ROADMAP.md diff --git a/standards/lotus-family/ROADMAP.md b/standards/lotus-family/ROADMAP.md new file mode 100644 index 00000000..af2f7268 --- /dev/null +++ b/standards/lotus-family/ROADMAP.md @@ -0,0 +1,17 @@ +# Lotus Family auditor roadmap + +## v0.1 + +- manifest for Pythia, CML, and LS; +- exact repository ref and commit SHA requirement; +- `PASS / DRIFT / UNKNOWN` outcomes; +- bilingual contract, authority firewall, regression test, and CI discovery checks; +- SHA-256 evidence artifact; +- audit-only authority boundary. + +## Follow-up + +- materialize exact snapshots from a trusted integration; +- verify remote repository identity and commit reachability; +- aggregate three repository results into one family report; +- sign or attest evidence artifacts without granting execution authority. From 1c43eecb3c8c93cd92a33d2cd2bf122cf724c1e5 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:24:23 +0300 Subject: [PATCH 15/79] docs: record Lotus Family implementation slice --- standards/lotus-family/IMPLEMENTATION.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 standards/lotus-family/IMPLEMENTATION.md diff --git a/standards/lotus-family/IMPLEMENTATION.md b/standards/lotus-family/IMPLEMENTATION.md new file mode 100644 index 00000000..174aee19 --- /dev/null +++ b/standards/lotus-family/IMPLEMENTATION.md @@ -0,0 +1,5 @@ +# Implementation slice v0.1 + +This first slice turns the Lotus Family strength map into executable checks. +It intentionally audits materialized exact snapshots and does not perform remote +repository access or consequential actions. From 22dee84fb1639fa461d182211a791471a74fff88 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:24:29 +0300 Subject: [PATCH 16/79] docs: state Lotus Family acceptance criteria --- standards/lotus-family/ACCEPTANCE.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 standards/lotus-family/ACCEPTANCE.md diff --git a/standards/lotus-family/ACCEPTANCE.md b/standards/lotus-family/ACCEPTANCE.md new file mode 100644 index 00000000..f2dbc837 --- /dev/null +++ b/standards/lotus-family/ACCEPTANCE.md @@ -0,0 +1,7 @@ +# Acceptance criteria + +- Removing a required English or Russian contract term produces `DRIFT`. +- A regression test that is not discovered by configured CI produces `DRIFT`. +- Missing snapshots, refs, or exact commit identities produce `UNKNOWN`, never `PASS`. +- Evidence records checked file paths and SHA-256 hashes. +- Audit results remain advisory and grant no execution or merge authority. From 01a293f28783a074649eee21daf327735f3dba39 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:34:06 +0300 Subject: [PATCH 17/79] fix: fail closed on Lotus manifest drift --- .../conformance/lotus_family_auditor.py | 366 +++++++++++++++--- 1 file changed, 307 insertions(+), 59 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_auditor.py b/standards/lotus-family/conformance/lotus_family_auditor.py index a7ff7637..381e7adb 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor.py +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -4,7 +4,8 @@ import hashlib import json import re -from pathlib import Path +import shlex +from pathlib import Path, PurePosixPath from typing import Any, Mapping PASS = "PASS" @@ -12,6 +13,7 @@ UNKNOWN = "UNKNOWN" _COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") +_PYTEST_START = ("python", "-m", "pytest") def load_json_object(path: Path) -> dict[str, Any]: @@ -21,15 +23,139 @@ def load_json_object(path: Path) -> dict[str, Any]: return value -def load_manifest(path: Path) -> dict[str, Any]: - manifest = load_json_object(path) +def _non_empty_string(value: Any, field: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field} must be a non-empty string") + return value + + +def _relative_manifest_path(value: Any, field: str) -> str: + path_text = _non_empty_string(value, field) + if "\\" in path_text: + raise ValueError(f"{field} must use repository-style '/' separators") + path = PurePosixPath(path_text) + if path.is_absolute() or path == PurePosixPath(".") or ".." in path.parts: + raise ValueError(f"{field} must stay inside the repository snapshot") + return path.as_posix() + + +def _validate_manifest(manifest: Mapping[str, Any]) -> None: if manifest.get("schema_version") != "pythia.lotus_family_manifest.v0.1": raise ValueError("unsupported Lotus Family manifest schema") if manifest.get("authority") != "audit_only": raise ValueError("Lotus Family manifest authority must be audit_only") + repositories = manifest.get("repositories") if not isinstance(repositories, list) or not repositories: raise ValueError("manifest repositories must be a non-empty list") + + repository_ids: set[str] = set() + snapshot_dirs: set[str] = set() + for repository_index, repository in enumerate(repositories): + prefix = f"repositories[{repository_index}]" + if not isinstance(repository, Mapping): + raise ValueError(f"{prefix} must be an object") + + repository_id = _non_empty_string(repository.get("id"), f"{prefix}.id") + if repository_id in repository_ids: + raise ValueError(f"duplicate repository id: {repository_id}") + repository_ids.add(repository_id) + + _non_empty_string(repository.get("repository"), f"{prefix}.repository") + snapshot_dir = _relative_manifest_path( + repository.get("snapshot_dir"), f"{prefix}.snapshot_dir" + ) + if "/" in snapshot_dir: + raise ValueError(f"{prefix}.snapshot_dir must be one directory name") + if snapshot_dir in snapshot_dirs: + raise ValueError(f"duplicate snapshot_dir: {snapshot_dir}") + snapshot_dirs.add(snapshot_dir) + + file_checks = repository.get("file_checks") + if not isinstance(file_checks, list) or not file_checks: + raise ValueError(f"{prefix}.file_checks must be a non-empty list") + + check_ids: set[str] = set() + checked_paths: set[str] = set() + for check_index, check in enumerate(file_checks): + check_prefix = f"{prefix}.file_checks[{check_index}]" + if not isinstance(check, Mapping): + raise ValueError(f"{check_prefix} must be an object") + + check_id = _non_empty_string(check.get("id"), f"{check_prefix}.id") + if check_id in check_ids: + raise ValueError(f"duplicate check id in {repository_id}: {check_id}") + check_ids.add(check_id) + + checked_paths.add( + _relative_manifest_path(check.get("path"), f"{check_prefix}.path") + ) + terms = check.get("contains_all") + if not isinstance(terms, list) or not terms: + raise ValueError( + f"{check_prefix}.contains_all must be a non-empty list" + ) + for term_index, term in enumerate(terms): + _non_empty_string( + term, f"{check_prefix}.contains_all[{term_index}]" + ) + + discovery = repository.get("ci_discovery") + if not isinstance(discovery, Mapping): + raise ValueError(f"{prefix}.ci_discovery must be an object") + + workflow_paths = discovery.get("workflow_paths") + if not isinstance(workflow_paths, list) or not workflow_paths: + raise ValueError( + f"{prefix}.ci_discovery.workflow_paths must be a non-empty list" + ) + for path_index, workflow_path in enumerate(workflow_paths): + _relative_manifest_path( + workflow_path, + f"{prefix}.ci_discovery.workflow_paths[{path_index}]", + ) + + strategy = discovery.get("strategy", "contains_any") + if strategy == "contains_any": + patterns = discovery.get("contains_any") + if not isinstance(patterns, list) or not patterns: + raise ValueError( + f"{prefix}.ci_discovery.contains_any must be a non-empty list" + ) + for pattern_index, pattern in enumerate(patterns): + _non_empty_string( + pattern, + f"{prefix}.ci_discovery.contains_any[{pattern_index}]", + ) + elif strategy == "pytest_default_discovery": + command = _non_empty_string( + discovery.get("command"), f"{prefix}.ci_discovery.command" + ) + if command != "python -m pytest": + raise ValueError( + f"{prefix}.ci_discovery.command must be 'python -m pytest'" + ) + test_path = _relative_manifest_path( + discovery.get("test_path"), f"{prefix}.ci_discovery.test_path" + ) + test_name = PurePosixPath(test_path).name + if not (test_name.startswith("test_") and test_name.endswith(".py")): + raise ValueError( + f"{prefix}.ci_discovery.test_path is not pytest-discoverable" + ) + if test_path not in checked_paths: + raise ValueError( + f"{prefix}.ci_discovery.test_path must also be a checked file" + ) + else: + raise ValueError( + f"{prefix}.ci_discovery.strategy is unsupported: {strategy}" + ) + + +def load_manifest(path: Path) -> dict[str, Any]: + manifest = load_json_object(path) + _validate_manifest(manifest) return manifest @@ -73,6 +199,28 @@ def _result( } +def _manifest_invalid_result( + *, + detail: str, + repository_id: str, + repository_ref: str, + commit_sha: str, + manifest_schema: str, +) -> dict[str, Any]: + return _result( + outcome=UNKNOWN, + reason_code="MANIFEST_INVALID", + detail=detail, + repository="", + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + + def _repository_config( manifest: Mapping[str, Any], repository_id: str ) -> Mapping[str, Any] | None: @@ -82,12 +230,25 @@ def _repository_config( return None +def _contained_path(root: Path, relative_path: str) -> tuple[Path | None, str | None]: + try: + normalized = _relative_manifest_path(relative_path, "manifest path") + root_resolved = root.resolve() + path = (root_resolved / Path(*PurePosixPath(normalized).parts)).resolve() + path.relative_to(root_resolved) + return path, None + except (OSError, ValueError): + return None, f"path escapes repository snapshot: {relative_path}" + + def _read_file( root: Path, relative_path: str, files: list[dict[str, str]], ) -> tuple[str | None, str | None]: - path = root / relative_path + path, containment_error = _contained_path(root, relative_path) + if containment_error is not None or path is None: + return None, containment_error if not path.is_file(): return None, f"required file is missing: {relative_path}" try: @@ -98,6 +259,75 @@ def _read_file( return None, f"cannot read {relative_path}: {exc.__class__.__name__}" +def _shell_commands(text: str) -> list[str]: + lines = text.splitlines() + commands: list[str] = [] + index = 0 + while index < len(lines): + stripped = lines[index].strip() + if "python -m pytest" not in stripped: + index += 1 + continue + + parts = [stripped] + while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): + parts[-1] = parts[-1].rstrip()[:-1] + index += 1 + parts.append(lines[index].strip()) + commands.append(" ".join(parts)) + index += 1 + return commands + + +def _is_pytest_default_discovery(command: str, test_path: str) -> bool: + try: + tokens = shlex.split(command) + except ValueError: + return False + + start = None + for index in range(len(tokens) - 2): + if tuple(tokens[index : index + 3]) == _PYTEST_START: + start = index + 3 + break + if start is None: + return False + + arguments = tokens[start:] + forbidden_options = {"--collect-only", "--co", "--setup-only"} + if any(argument in forbidden_options for argument in arguments): + return False + if any(test_path in argument for argument in arguments): + return False + + # Fail closed on positional selectors. The supported full-discovery form may + # carry options, but it may not name a test file, directory, or node id. + return all(argument.startswith("-") for argument in arguments) + + +def _ci_discovery( + discovery: Mapping[str, Any], + combined: str, +) -> tuple[bool, list[str]]: + strategy = discovery.get("strategy", "contains_any") + if strategy == "contains_any": + patterns = [str(pattern) for pattern in discovery["contains_any"]] + matches = [pattern for pattern in patterns if pattern in combined] + return bool(matches), matches + + if strategy == "pytest_default_discovery": + command_name = str(discovery["command"]) + test_path = str(discovery["test_path"]) + matches = [ + command + for command in _shell_commands(combined) + if _is_pytest_default_discovery(command, test_path) + ] + return bool(matches), [command_name] if matches else [] + + return False, [] + + def audit_repository( manifest: Mapping[str, Any], *, @@ -107,6 +337,17 @@ def audit_repository( commit_sha: str, ) -> dict[str, Any]: manifest_schema = str(manifest.get("schema_version", "")) + try: + _validate_manifest(manifest) + except (TypeError, ValueError) as exc: + return _manifest_invalid_result( + detail=str(exc), + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + ) + config = _repository_config(manifest, repository_id) if config is None: return _result( @@ -150,7 +391,26 @@ def audit_repository( files=[], ) - repository_root = snapshot_root / str(config["snapshot_dir"]) + snapshot_root_resolved = snapshot_root.resolve() + repository_root = ( + snapshot_root_resolved / str(config["snapshot_dir"]) + ).resolve() + try: + repository_root.relative_to(snapshot_root_resolved) + except ValueError: + return _result( + outcome=UNKNOWN, + reason_code="SNAPSHOT_UNAVAILABLE", + detail="repository snapshot escapes snapshot_root", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + if not repository_root.is_dir(): return _result( outcome=UNKNOWN, @@ -169,7 +429,7 @@ def audit_repository( files: list[dict[str, str]] = [] drift: list[str] = [] - for check in config.get("file_checks", []): + for check in config["file_checks"]: check_id = str(check["id"]) relative_path = str(check["path"]) text, error = _read_file(repository_root, relative_path, files) @@ -186,9 +446,8 @@ def audit_repository( drift.append(check_id) continue - missing_terms = [ - term for term in check.get("contains_all", []) if term not in text - ] + terms = [str(term) for term in check["contains_all"]] + missing_terms = [term for term in terms if term not in text] outcome = PASS if not missing_terms else DRIFT checks.append( { @@ -206,43 +465,39 @@ def audit_repository( if outcome == DRIFT: drift.append(check_id) - discovery = config.get("ci_discovery") - if discovery: - workflow_paths = [str(path) for path in discovery["workflow_paths"]] - workflow_texts: list[str] = [] - read_errors: list[str] = [] - for relative_path in workflow_paths: - text, error = _read_file(repository_root, relative_path, files) - if error is not None: - read_errors.append(error) - else: - workflow_texts.append(text) - - combined = "\n".join(workflow_texts) - patterns = [str(pattern) for pattern in discovery["contains_any"]] - discovered = any(pattern in combined for pattern in patterns) - if read_errors and not workflow_texts: - outcome = DRIFT - detail = "; ".join(read_errors) - drift.append("ci_discovery") - elif discovered: - outcome = PASS - detail = "contract regression test is discovered by CI" + discovery = config["ci_discovery"] + workflow_paths = [str(path) for path in discovery["workflow_paths"]] + workflow_texts: list[str] = [] + read_errors: list[str] = [] + for relative_path in workflow_paths: + text, error = _read_file(repository_root, relative_path, files) + if error is not None: + read_errors.append(error) else: - outcome = DRIFT - detail = "no configured CI discovery pattern was found" - drift.append("ci_discovery") - checks.append( - { - "check_id": "ci_discovery", - "outcome": outcome, - "paths": workflow_paths, - "matched_patterns": [ - pattern for pattern in patterns if pattern in combined - ], - "detail": detail, - } - ) + workflow_texts.append(text) + + combined = "\n".join(workflow_texts) + discovered, matched_patterns = _ci_discovery(discovery, combined) + if read_errors and not workflow_texts: + outcome = DRIFT + detail = "; ".join(read_errors) + drift.append("ci_discovery") + elif discovered: + outcome = PASS + detail = "contract regression test is discovered by CI" + else: + outcome = DRIFT + detail = "no configured contract-specific CI discovery rule matched" + drift.append("ci_discovery") + checks.append( + { + "check_id": "ci_discovery", + "outcome": outcome, + "paths": workflow_paths, + "matched_patterns": matched_patterns, + "detail": detail, + } + ) if drift: outcome = DRIFT @@ -290,20 +545,13 @@ def main(argv: list[str] | None = None) -> int: commit_sha=args.commit_sha, ) except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: - result = { - "schema_version": "pythia.lotus_family_audit_result.v0.1", - "outcome": UNKNOWN, - "reason_code": "MANIFEST_INVALID", - "detail": str(exc), - "authority": { - "mode": "audit_only", - "grants_ownership": False, - "grants_approval": False, - "grants_execution": False, - "grants_delivery": False, - "grants_merge": False, - }, - } + result = _manifest_invalid_result( + detail=str(exc), + repository_id=args.repository_id, + repository_ref=args.repository_ref, + commit_sha=args.commit_sha, + manifest_schema="", + ) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text( From b85f5433d910292cf3a6c2e0243af64ae0765c2e Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:34:18 +0300 Subject: [PATCH 18/79] fix: bind CML audit to full pytest discovery --- standards/lotus-family/manifest/lotus-family-v0.1.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/standards/lotus-family/manifest/lotus-family-v0.1.json b/standards/lotus-family/manifest/lotus-family-v0.1.json index 6b9e2b26..4cece715 100644 --- a/standards/lotus-family/manifest/lotus-family-v0.1.json +++ b/standards/lotus-family/manifest/lotus-family-v0.1.json @@ -57,6 +57,7 @@ "workflow_paths": [ ".github/workflows/ci.yml" ], + "strategy": "contains_any", "contains_any": [ "mix test" ] @@ -101,9 +102,9 @@ "workflow_paths": [ ".github/workflows/ci.yml" ], - "contains_any": [ - "python -m pytest" - ] + "strategy": "pytest_default_discovery", + "command": "python -m pytest", + "test_path": "tests/test_lotus_docs_contract.py" } }, { @@ -147,6 +148,7 @@ "workflow_paths": [ ".github/workflows/ci.yml" ], + "strategy": "contains_any", "contains_any": [ "tests/test_lotus_docs_contract.py" ] From 33cfcaef44e42190d7d1f6af9140b95357945075 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:34:37 +0300 Subject: [PATCH 19/79] test: cover manifest and CI discovery drift --- .../conformance/test_lotus_family_auditor.py | 103 +++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/standards/lotus-family/conformance/test_lotus_family_auditor.py b/standards/lotus-family/conformance/test_lotus_family_auditor.py index 96af48c5..7a1c2f6a 100644 --- a/standards/lotus-family/conformance/test_lotus_family_auditor.py +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -19,6 +19,12 @@ def _write(root: Path, relative_path: str, content: str) -> None: path.write_text(content, encoding="utf-8") +def _discovery_fixture(discovery: dict) -> str: + if discovery.get("strategy") == "pytest_default_discovery": + return "python -m pytest \\\n --junitxml=artifacts/junit.xml \\\n --cov=cml\n" + return "\n".join(discovery["contains_any"]) + "\n" + + def _materialize_repository(snapshot_root: Path, config: dict) -> Path: repository_root = snapshot_root / config["snapshot_dir"] terms_by_path: dict[str, list[str]] = {} @@ -30,7 +36,7 @@ def _materialize_repository(snapshot_root: Path, config: dict) -> Path: _write( repository_root, discovery["workflow_paths"][0], - "\n".join(discovery["contains_any"]) + "\n", + _discovery_fixture(discovery), ) return repository_root @@ -79,6 +85,19 @@ def test_valid_pythia_snapshot_passes_with_exact_identity_and_hashes(self): self.assertRegex(row["sha256"], r"^[0-9a-f]{64}$") self.assertFalse(result["authority"]["grants_merge"]) + def test_valid_cml_default_pytest_discovery_passes(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("cml")) + + result = self.audit("cml", snapshot_root) + + self.assertEqual(result["outcome"], PASS) + discovery = next( + row for row in result["checks"] if row["check_id"] == "ci_discovery" + ) + self.assertEqual(discovery["matched_patterns"], ["python -m pytest"]) + def test_bilingual_authority_deletion_is_drift(self): with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) @@ -123,6 +142,88 @@ def test_contract_test_not_discovered_by_ci_is_drift(self): self.assertEqual(discovery["outcome"], DRIFT) self.assertEqual(discovery["matched_patterns"], []) + def test_cml_unrelated_pytest_subset_is_not_full_discovery(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + config = self.config("cml") + repository_root = _materialize_repository(snapshot_root, config) + workflow = repository_root / ".github/workflows/ci.yml" + workflow.write_text( + "python -m pytest tests/test_other.py\n", + encoding="utf-8", + ) + + result = self.audit("cml", snapshot_root) + + self.assertEqual(result["outcome"], DRIFT) + discovery = next( + row for row in result["checks"] if row["check_id"] == "ci_discovery" + ) + self.assertEqual(discovery["outcome"], DRIFT) + self.assertEqual(discovery["matched_patterns"], []) + + def test_cml_pytest_ignore_of_contract_test_is_drift(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + config = self.config("cml") + repository_root = _materialize_repository(snapshot_root, config) + workflow = repository_root / ".github/workflows/ci.yml" + workflow.write_text( + "python -m pytest --ignore=tests/test_lotus_docs_contract.py\n", + encoding="utf-8", + ) + + result = self.audit("cml", snapshot_root) + + self.assertEqual(result["outcome"], DRIFT) + discovery = next( + row for row in result["checks"] if row["check_id"] == "ci_discovery" + ) + self.assertEqual(discovery["outcome"], DRIFT) + + def test_empty_manifest_term_list_is_unknown_not_pass(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("pythia")) + manifest = copy.deepcopy(self.manifest) + manifest["repositories"][0]["file_checks"][0]["contains_all"] = [] + + result = audit_repository( + manifest, + repository_id="pythia", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + self.assertEqual( + (result["outcome"], result["reason_code"]), + (UNKNOWN, "MANIFEST_INVALID"), + ) + + def test_manifest_path_traversal_is_unknown_and_not_hashed(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("pythia")) + outside = snapshot_root / "outside.md" + outside.write_text("all required terms\n", encoding="utf-8") + manifest = copy.deepcopy(self.manifest) + manifest["repositories"][0]["file_checks"][0]["path"] = "../outside.md" + + result = audit_repository( + manifest, + repository_id="pythia", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + self.assertEqual( + (result["outcome"], result["reason_code"]), + (UNKNOWN, "MANIFEST_INVALID"), + ) + self.assertEqual(result["files"], []) + def test_missing_repository_snapshot_is_unknown_not_pass(self): with tempfile.TemporaryDirectory() as directory: result = self.audit("pythia", Path(directory)) From 258ea44c92731301068a902c1bd5e8d692912534 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:44:08 +0300 Subject: [PATCH 20/79] fix: harden pytest CI discovery parsing --- .../conformance/lotus_family_auditor.py | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_auditor.py b/standards/lotus-family/conformance/lotus_family_auditor.py index 381e7adb..fbc360bd 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor.py +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import fnmatch import hashlib import json import re @@ -259,12 +260,34 @@ def _read_file( return None, f"cannot read {relative_path}: {exc.__class__.__name__}" +def _strip_unquoted_shell_comment(line: str) -> str: + in_single = False + in_double = False + escaped = False + for index, character in enumerate(line): + if escaped: + escaped = False + continue + if character == "\\" and not in_single: + escaped = True + continue + if character == "'" and not in_double: + in_single = not in_single + continue + if character == '"' and not in_single: + in_double = not in_double + continue + if character == "#" and not in_single and not in_double: + return line[:index] + return line + + def _shell_commands(text: str) -> list[str]: lines = text.splitlines() commands: list[str] = [] index = 0 while index < len(lines): - stripped = lines[index].strip() + stripped = _strip_unquoted_shell_comment(lines[index]).strip() if "python -m pytest" not in stripped: index += 1 continue @@ -273,12 +296,39 @@ def _shell_commands(text: str) -> list[str]: while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): parts[-1] = parts[-1].rstrip()[:-1] index += 1 - parts.append(lines[index].strip()) + continuation = _strip_unquoted_shell_comment(lines[index]).strip() + if continuation: + parts.append(continuation) commands.append(" ".join(parts)) index += 1 return commands +def _ignore_path_covers_test(ignore_path: str, test_path: str) -> bool: + normalized = ignore_path.strip().replace("\\", "/").rstrip("/") + if not normalized: + return True + ignored = PurePosixPath(normalized) + test = PurePosixPath(test_path) + if ignored.is_absolute() or ".." in ignored.parts: + return True + return ignored == test or ignored in test.parents + + +def _ignore_glob_covers_test(pattern: str, test_path: str) -> bool: + normalized = pattern.strip().replace("\\", "/") + if not normalized: + return True + test = PurePosixPath(test_path) + candidates = [test.as_posix(), test.name] + candidates.extend( + parent.as_posix() + for parent in test.parents + if parent != PurePosixPath(".") + ) + return any(fnmatch.fnmatchcase(candidate, normalized) for candidate in candidates) + + def _is_pytest_default_discovery(command: str, test_path: str) -> bool: try: tokens = shlex.split(command) @@ -300,6 +350,24 @@ def _is_pytest_default_discovery(command: str, test_path: str) -> bool: if any(test_path in argument for argument in arguments): return False + for index, argument in enumerate(arguments): + if argument.startswith("--ignore="): + if _ignore_path_covers_test(argument.split("=", 1)[1], test_path): + return False + elif argument == "--ignore": + if index + 1 >= len(arguments): + return False + if _ignore_path_covers_test(arguments[index + 1], test_path): + return False + elif argument.startswith("--ignore-glob="): + if _ignore_glob_covers_test(argument.split("=", 1)[1], test_path): + return False + elif argument == "--ignore-glob": + if index + 1 >= len(arguments): + return False + if _ignore_glob_covers_test(arguments[index + 1], test_path): + return False + # Fail closed on positional selectors. The supported full-discovery form may # carry options, but it may not name a test file, directory, or node id. return all(argument.startswith("-") for argument in arguments) From d85abbc5d0cbc3997cf7347859b6e86ef5bba1df Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:44:31 +0300 Subject: [PATCH 21/79] test: cover commented and ignored pytest commands --- .../conformance/test_lotus_family_auditor.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/standards/lotus-family/conformance/test_lotus_family_auditor.py b/standards/lotus-family/conformance/test_lotus_family_auditor.py index 7a1c2f6a..769b6196 100644 --- a/standards/lotus-family/conformance/test_lotus_family_auditor.py +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -181,6 +181,60 @@ def test_cml_pytest_ignore_of_contract_test_is_drift(self): ) self.assertEqual(discovery["outcome"], DRIFT) + def test_cml_commented_pytest_command_is_drift(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + config = self.config("cml") + repository_root = _materialize_repository(snapshot_root, config) + workflow = repository_root / ".github/workflows/ci.yml" + workflow.write_text("# python -m pytest\n", encoding="utf-8") + + result = self.audit("cml", snapshot_root) + + self.assertEqual(result["outcome"], DRIFT) + discovery = next( + row for row in result["checks"] if row["check_id"] == "ci_discovery" + ) + self.assertEqual(discovery["matched_patterns"], []) + + def test_cml_pytest_ignore_directory_covering_contract_test_is_drift(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + config = self.config("cml") + repository_root = _materialize_repository(snapshot_root, config) + workflow = repository_root / ".github/workflows/ci.yml" + workflow.write_text( + "python -m pytest --ignore=tests\n", + encoding="utf-8", + ) + + result = self.audit("cml", snapshot_root) + + self.assertEqual(result["outcome"], DRIFT) + discovery = next( + row for row in result["checks"] if row["check_id"] == "ci_discovery" + ) + self.assertEqual(discovery["matched_patterns"], []) + + def test_cml_pytest_ignore_glob_covering_contract_test_is_drift(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + config = self.config("cml") + repository_root = _materialize_repository(snapshot_root, config) + workflow = repository_root / ".github/workflows/ci.yml" + workflow.write_text( + "python -m pytest --ignore-glob='tests/*'\n", + encoding="utf-8", + ) + + result = self.audit("cml", snapshot_root) + + self.assertEqual(result["outcome"], DRIFT) + discovery = next( + row for row in result["checks"] if row["check_id"] == "ci_discovery" + ) + self.assertEqual(discovery["matched_patterns"], []) + def test_empty_manifest_term_list_is_unknown_not_pass(self): with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) From d0828c5ab4038f5d188cadf6d46be23858ac9dc5 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:58:00 +0300 Subject: [PATCH 22/79] fix: make CI discovery fail closed --- .../conformance/lotus_family_auditor.py | 305 +++++++++++------- 1 file changed, 189 insertions(+), 116 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_auditor.py b/standards/lotus-family/conformance/lotus_family_auditor.py index fbc360bd..44d528d0 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor.py +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -14,7 +14,20 @@ UNKNOWN = "UNKNOWN" _COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") -_PYTEST_START = ("python", "-m", "pytest") +_PYTEST_STARTS = (("python", "-m", "pytest"), ("pytest",)) +_SAFE_PYTEST_FLAGS = { + "-q", "--quiet", "-v", "--verbose", "--strict-markers", + "--strict-config", "--disable-warnings", +} +_SAFE_PYTEST_PREFIXES = ( + "--junitxml=", "--cov=", "--cov-report=", "--color=", "--tb=", + "--durations=", "--maxfail=", +) +_FORBIDDEN_PYTEST_FLAGS = { + "--collect-only", "--co", "--setup-only", "--pyargs", + "-k", "--keyword", "-m", "--markers", "--deselect", +} +_SHELL_CONTROL = {"&&", "||", ";", "|", "&"} def load_json_object(path: Path) -> dict[str, Any]: @@ -31,10 +44,10 @@ def _non_empty_string(value: Any, field: str) -> str: def _relative_manifest_path(value: Any, field: str) -> str: - path_text = _non_empty_string(value, field) - if "\\" in path_text: + text = _non_empty_string(value, field) + if "\\" in text: raise ValueError(f"{field} must use repository-style '/' separators") - path = PurePosixPath(path_text) + path = PurePosixPath(text) if path.is_absolute() or path == PurePosixPath(".") or ".." in path.parts: raise ValueError(f"{field} must stay inside the repository snapshot") return path.as_posix() @@ -45,49 +58,43 @@ def _validate_manifest(manifest: Mapping[str, Any]) -> None: raise ValueError("unsupported Lotus Family manifest schema") if manifest.get("authority") != "audit_only": raise ValueError("Lotus Family manifest authority must be audit_only") - repositories = manifest.get("repositories") if not isinstance(repositories, list) or not repositories: raise ValueError("manifest repositories must be a non-empty list") - repository_ids: set[str] = set() - snapshot_dirs: set[str] = set() - for repository_index, repository in enumerate(repositories): - prefix = f"repositories[{repository_index}]" - if not isinstance(repository, Mapping): + ids: set[str] = set() + dirs: set[str] = set() + for repo_index, repo in enumerate(repositories): + prefix = f"repositories[{repo_index}]" + if not isinstance(repo, Mapping): raise ValueError(f"{prefix} must be an object") - - repository_id = _non_empty_string(repository.get("id"), f"{prefix}.id") - if repository_id in repository_ids: - raise ValueError(f"duplicate repository id: {repository_id}") - repository_ids.add(repository_id) - - _non_empty_string(repository.get("repository"), f"{prefix}.repository") + repo_id = _non_empty_string(repo.get("id"), f"{prefix}.id") + if repo_id in ids: + raise ValueError(f"duplicate repository id: {repo_id}") + ids.add(repo_id) + _non_empty_string(repo.get("repository"), f"{prefix}.repository") snapshot_dir = _relative_manifest_path( - repository.get("snapshot_dir"), f"{prefix}.snapshot_dir" + repo.get("snapshot_dir"), f"{prefix}.snapshot_dir" ) if "/" in snapshot_dir: raise ValueError(f"{prefix}.snapshot_dir must be one directory name") - if snapshot_dir in snapshot_dirs: + if snapshot_dir in dirs: raise ValueError(f"duplicate snapshot_dir: {snapshot_dir}") - snapshot_dirs.add(snapshot_dir) + dirs.add(snapshot_dir) - file_checks = repository.get("file_checks") - if not isinstance(file_checks, list) or not file_checks: + checks = repo.get("file_checks") + if not isinstance(checks, list) or not checks: raise ValueError(f"{prefix}.file_checks must be a non-empty list") - check_ids: set[str] = set() checked_paths: set[str] = set() - for check_index, check in enumerate(file_checks): + for check_index, check in enumerate(checks): check_prefix = f"{prefix}.file_checks[{check_index}]" if not isinstance(check, Mapping): raise ValueError(f"{check_prefix} must be an object") - check_id = _non_empty_string(check.get("id"), f"{check_prefix}.id") if check_id in check_ids: - raise ValueError(f"duplicate check id in {repository_id}: {check_id}") + raise ValueError(f"duplicate check id in {repo_id}: {check_id}") check_ids.add(check_id) - checked_paths.add( _relative_manifest_path(check.get("path"), f"{check_prefix}.path") ) @@ -101,22 +108,21 @@ def _validate_manifest(manifest: Mapping[str, Any]) -> None: term, f"{check_prefix}.contains_all[{term_index}]" ) - discovery = repository.get("ci_discovery") + discovery = repo.get("ci_discovery") if not isinstance(discovery, Mapping): raise ValueError(f"{prefix}.ci_discovery must be an object") - - workflow_paths = discovery.get("workflow_paths") - if not isinstance(workflow_paths, list) or not workflow_paths: + workflows = discovery.get("workflow_paths") + if not isinstance(workflows, list) or not workflows: raise ValueError( f"{prefix}.ci_discovery.workflow_paths must be a non-empty list" ) - for path_index, workflow_path in enumerate(workflow_paths): + for path_index, workflow_path in enumerate(workflows): _relative_manifest_path( workflow_path, f"{prefix}.ci_discovery.workflow_paths[{path_index}]", ) - strategy = discovery.get("strategy", "contains_any") + strategy = discovery.get("strategy") if strategy == "contains_any": patterns = discovery.get("contains_any") if not isinstance(patterns, list) or not patterns: @@ -201,7 +207,6 @@ def _result( def _manifest_invalid_result( - *, detail: str, repository_id: str, repository_ref: str, @@ -225,13 +230,15 @@ def _manifest_invalid_result( def _repository_config( manifest: Mapping[str, Any], repository_id: str ) -> Mapping[str, Any] | None: - for row in manifest["repositories"]: - if row.get("id") == repository_id: - return row - return None + return next( + (row for row in manifest["repositories"] if row.get("id") == repository_id), + None, + ) -def _contained_path(root: Path, relative_path: str) -> tuple[Path | None, str | None]: +def _contained_path( + root: Path, relative_path: str +) -> tuple[Path | None, str | None]: try: normalized = _relative_manifest_path(relative_path, "manifest path") root_resolved = root.resolve() @@ -247,9 +254,9 @@ def _read_file( relative_path: str, files: list[dict[str, str]], ) -> tuple[str | None, str | None]: - path, containment_error = _contained_path(root, relative_path) - if containment_error is not None or path is None: - return None, containment_error + path, error = _contained_path(root, relative_path) + if error is not None or path is None: + return None, error if not path.is_file(): return None, f"required file is missing: {relative_path}" try: @@ -287,12 +294,11 @@ def _shell_commands(text: str) -> list[str]: commands: list[str] = [] index = 0 while index < len(lines): - stripped = _strip_unquoted_shell_comment(lines[index]).strip() - if "python -m pytest" not in stripped: + first = _strip_unquoted_shell_comment(lines[index]).strip() + if not first: index += 1 continue - - parts = [stripped] + parts = [first] while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): parts[-1] = parts[-1].rstrip()[:-1] index += 1 @@ -304,8 +310,37 @@ def _shell_commands(text: str) -> list[str]: return commands +def _tokenize_executed_command(command: str) -> list[str] | None: + try: + tokens = shlex.split(command) + except ValueError: + return None + if not tokens or any(token in _SHELL_CONTROL for token in tokens): + return None + index = 0 + while index < len(tokens) and re.fullmatch( + r"[A-Za-z_][A-Za-z0-9_]*=.*", tokens[index] + ): + index += 1 + return tokens[index:] or None + + +def _pytest_parts(tokens: list[str]) -> tuple[list[str], list[str]] | None: + for start in _PYTEST_STARTS: + if tuple(tokens[: len(start)]) == start: + return list(start), tokens[len(start) :] + return None + + +def _normalize_repo_pattern(value: str) -> str: + normalized = value.strip().replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + return normalized.rstrip("/") + + def _ignore_path_covers_test(ignore_path: str, test_path: str) -> bool: - normalized = ignore_path.strip().replace("\\", "/").rstrip("/") + normalized = _normalize_repo_pattern(ignore_path) if not normalized: return True ignored = PurePosixPath(normalized) @@ -316,7 +351,7 @@ def _ignore_path_covers_test(ignore_path: str, test_path: str) -> bool: def _ignore_glob_covers_test(pattern: str, test_path: str) -> bool: - normalized = pattern.strip().replace("\\", "/") + normalized = _normalize_repo_pattern(pattern) if not normalized: return True test = PurePosixPath(test_path) @@ -326,31 +361,22 @@ def _ignore_glob_covers_test(pattern: str, test_path: str) -> bool: for parent in test.parents if parent != PurePosixPath(".") ) - return any(fnmatch.fnmatchcase(candidate, normalized) for candidate in candidates) - - -def _is_pytest_default_discovery(command: str, test_path: str) -> bool: - try: - tokens = shlex.split(command) - except ValueError: - return False + return any( + fnmatch.fnmatchcase(candidate, normalized) for candidate in candidates + ) - start = None - for index in range(len(tokens) - 2): - if tuple(tokens[index : index + 3]) == _PYTEST_START: - start = index + 3 - break - if start is None: - return False - arguments = tokens[start:] - forbidden_options = {"--collect-only", "--co", "--setup-only"} - if any(argument in forbidden_options for argument in arguments): - return False - if any(test_path in argument for argument in arguments): - return False - - for index, argument in enumerate(arguments): +def _pytest_arguments_safe( + arguments: list[str], test_path: str, *, require_test_path: bool +) -> bool: + positive_paths: list[str] = [] + index = 0 + while index < len(arguments): + argument = arguments[index] + if argument in _FORBIDDEN_PYTEST_FLAGS or argument.startswith( + ("-k=", "--keyword=", "-m=", "--markers=", "--deselect=") + ): + return False if argument.startswith("--ignore="): if _ignore_path_covers_test(argument.split("=", 1)[1], test_path): return False @@ -359,6 +385,7 @@ def _is_pytest_default_discovery(command: str, test_path: str) -> bool: return False if _ignore_path_covers_test(arguments[index + 1], test_path): return False + index += 1 elif argument.startswith("--ignore-glob="): if _ignore_glob_covers_test(argument.split("=", 1)[1], test_path): return False @@ -367,33 +394,81 @@ def _is_pytest_default_discovery(command: str, test_path: str) -> bool: return False if _ignore_glob_covers_test(arguments[index + 1], test_path): return False + index += 1 + elif argument in _SAFE_PYTEST_FLAGS or argument.startswith( + _SAFE_PYTEST_PREFIXES + ): + pass + elif argument.startswith("-"): + return False + else: + positive_paths.append( + _normalize_repo_pattern(argument.split("::", 1)[0]) + ) + index += 1 + + normalized_test = _normalize_repo_pattern(test_path) + if require_test_path: + return normalized_test in positive_paths + return not positive_paths + + +def _is_pytest_default_discovery(command: str, test_path: str) -> bool: + tokens = _tokenize_executed_command(command) + if tokens is None: + return False + parts = _pytest_parts(tokens) + return parts is not None and _pytest_arguments_safe( + parts[1], test_path, require_test_path=False + ) - # Fail closed on positional selectors. The supported full-discovery form may - # carry options, but it may not name a test file, directory, or node id. - return all(argument.startswith("-") for argument in arguments) + +def _is_explicit_pytest_test_command(command: str, test_path: str) -> bool: + tokens = _tokenize_executed_command(command) + if tokens is None: + return False + parts = _pytest_parts(tokens) + return parts is not None and _pytest_arguments_safe( + parts[1], test_path, require_test_path=True + ) + + +def _is_command_prefix(command: str, prefix: str) -> bool: + tokens = _tokenize_executed_command(command) + if tokens is None: + return False + try: + required = shlex.split(prefix) + except ValueError: + return False + return tokens[: len(required)] == required def _ci_discovery( - discovery: Mapping[str, Any], - combined: str, + discovery: Mapping[str, Any], combined: str ) -> tuple[bool, list[str]]: - strategy = discovery.get("strategy", "contains_any") - if strategy == "contains_any": - patterns = [str(pattern) for pattern in discovery["contains_any"]] - matches = [pattern for pattern in patterns if pattern in combined] - return bool(matches), matches - + commands = _shell_commands(combined) + strategy = discovery["strategy"] if strategy == "pytest_default_discovery": - command_name = str(discovery["command"]) test_path = str(discovery["test_path"]) - matches = [ - command - for command in _shell_commands(combined) - if _is_pytest_default_discovery(command, test_path) - ] - return bool(matches), [command_name] if matches else [] - - return False, [] + matched = any( + _is_pytest_default_discovery(command, test_path) + for command in commands + ) + return matched, [str(discovery["command"])] if matched else [] + + matches: list[str] = [] + for pattern_value in discovery["contains_any"]: + pattern = str(pattern_value) + if pattern.endswith(".py"): + if any( + _is_explicit_pytest_test_command(command, pattern) + for command in commands + ): + matches.append(pattern) + elif any(_is_command_prefix(command, pattern) for command in commands): + matches.append(pattern) + return bool(matches), matches def audit_repository( @@ -409,11 +484,11 @@ def audit_repository( _validate_manifest(manifest) except (TypeError, ValueError) as exc: return _manifest_invalid_result( - detail=str(exc), - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, + str(exc), + repository_id, + repository_ref, + commit_sha, + manifest_schema, ) config = _repository_config(manifest, repository_id) @@ -478,7 +553,6 @@ def audit_repository( checks=[], files=[], ) - if not repository_root.is_dir(): return _result( outcome=UNKNOWN, @@ -496,12 +570,11 @@ def audit_repository( checks: list[dict[str, Any]] = [] files: list[dict[str, str]] = [] drift: list[str] = [] - for check in config["file_checks"]: check_id = str(check["id"]) relative_path = str(check["path"]) text, error = _read_file(repository_root, relative_path, files) - if error is not None: + if error is not None or text is None: checks.append( { "check_id": check_id, @@ -513,7 +586,6 @@ def audit_repository( ) drift.append(check_id) continue - terms = [str(term) for term in check["contains_all"]] missing_terms = [term for term in terms if term not in text] outcome = PASS if not missing_terms else DRIFT @@ -539,28 +611,29 @@ def audit_repository( read_errors: list[str] = [] for relative_path in workflow_paths: text, error = _read_file(repository_root, relative_path, files) - if error is not None: - read_errors.append(error) + if error is not None or text is None: + read_errors.append(error or f"cannot read {relative_path}") else: workflow_texts.append(text) - combined = "\n".join(workflow_texts) - discovered, matched_patterns = _ci_discovery(discovery, combined) + discovered, matched_patterns = _ci_discovery( + discovery, "\n".join(workflow_texts) + ) if read_errors and not workflow_texts: - outcome = DRIFT + ci_outcome = DRIFT detail = "; ".join(read_errors) - drift.append("ci_discovery") elif discovered: - outcome = PASS - detail = "contract regression test is discovered by CI" + ci_outcome = PASS + detail = "contract regression test is discovered by executable CI" else: - outcome = DRIFT - detail = "no configured contract-specific CI discovery rule matched" + ci_outcome = DRIFT + detail = "no configured executable CI discovery rule matched" + if ci_outcome == DRIFT: drift.append("ci_discovery") checks.append( { "check_id": "ci_discovery", - "outcome": outcome, + "outcome": ci_outcome, "paths": workflow_paths, "matched_patterns": matched_patterns, "detail": detail, @@ -614,11 +687,11 @@ def main(argv: list[str] | None = None) -> int: ) except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: result = _manifest_invalid_result( - detail=str(exc), - repository_id=args.repository_id, - repository_ref=args.repository_ref, - commit_sha=args.commit_sha, - manifest_schema="", + str(exc), + args.repository_id, + args.repository_ref, + args.commit_sha, + "", ) args.output.parent.mkdir(parents=True, exist_ok=True) From 9e6aee5b1a730f539fd6d309b03c640d9a769cc3 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:58:24 +0300 Subject: [PATCH 23/79] test: cover fail-closed CI command parsing --- .../conformance/test_lotus_family_auditor.py | 238 +++++++++--------- 1 file changed, 112 insertions(+), 126 deletions(-) diff --git a/standards/lotus-family/conformance/test_lotus_family_auditor.py b/standards/lotus-family/conformance/test_lotus_family_auditor.py index 769b6196..0a2cbd3d 100644 --- a/standards/lotus-family/conformance/test_lotus_family_auditor.py +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -21,8 +21,15 @@ def _write(root: Path, relative_path: str, content: str) -> None: def _discovery_fixture(discovery: dict) -> str: if discovery.get("strategy") == "pytest_default_discovery": - return "python -m pytest \\\n --junitxml=artifacts/junit.xml \\\n --cov=cml\n" - return "\n".join(discovery["contains_any"]) + "\n" + return ( + "python -m pytest \\\n" + " --junitxml=artifacts/junit.xml \\\n" + " --cov=cml\n" + ) + pattern = discovery["contains_any"][0] + if pattern.endswith(".py"): + return f"python -m pytest {pattern}\n" + return pattern + "\n" def _materialize_repository(snapshot_root: Path, config: dict) -> Path: @@ -31,7 +38,11 @@ def _materialize_repository(snapshot_root: Path, config: dict) -> Path: for check in config["file_checks"]: terms_by_path.setdefault(check["path"], []).extend(check["contains_all"]) for relative_path, terms in terms_by_path.items(): - _write(repository_root, relative_path, "\n".join(dict.fromkeys(terms)) + "\n") + _write( + repository_root, + relative_path, + "\n".join(dict.fromkeys(terms)) + "\n", + ) discovery = config["ci_discovery"] _write( repository_root, @@ -47,7 +58,9 @@ def setUp(self) -> None: def config(self, repository_id: str) -> dict: return next( - row for row in self.manifest["repositories"] if row["id"] == repository_id + row + for row in self.manifest["repositories"] + if row["id"] == repository_id ) def audit(self, repository_id: str, snapshot_root: Path): @@ -59,6 +72,26 @@ def audit(self, repository_id: str, snapshot_root: Path): commit_sha=SHA, ) + def workflow(self, repository_root: Path) -> Path: + return repository_root / ".github/workflows/ci.yml" + + def discovery(self, result: dict) -> dict: + return next( + row for row in result["checks"] if row["check_id"] == "ci_discovery" + ) + + def assert_ci_drift(self, repository_id: str, command: str) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_repository( + snapshot_root, self.config(repository_id) + ) + self.workflow(repository_root).write_text(command, encoding="utf-8") + result = self.audit(repository_id, snapshot_root) + self.assertEqual(result["outcome"], DRIFT) + self.assertEqual(self.discovery(result)["outcome"], DRIFT) + self.assertEqual(self.discovery(result)["matched_patterns"], []) + def test_manifest_has_three_repository_adapters_and_audit_only_authority(self): self.assertEqual(self.manifest["authority"], "audit_only") self.assertEqual( @@ -70,9 +103,7 @@ def test_valid_pythia_snapshot_passes_with_exact_identity_and_hashes(self): with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) _materialize_repository(snapshot_root, self.config("pythia")) - result = self.audit("pythia", snapshot_root) - self.assertEqual( (result["outcome"], result["reason_code"]), (PASS, "LOTUS_CONTRACT_CONFORMANT"), @@ -89,20 +120,30 @@ def test_valid_cml_default_pytest_discovery_passes(self): with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) _materialize_repository(snapshot_root, self.config("cml")) - result = self.audit("cml", snapshot_root) + self.assertEqual(result["outcome"], PASS) + self.assertEqual( + self.discovery(result)["matched_patterns"], + ["python -m pytest"], + ) + def test_valid_ls_explicit_contract_test_passes(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("ls")) + result = self.audit("ls", snapshot_root) self.assertEqual(result["outcome"], PASS) - discovery = next( - row for row in result["checks"] if row["check_id"] == "ci_discovery" + self.assertEqual( + self.discovery(result)["matched_patterns"], + ["tests/test_lotus_docs_contract.py"], ) - self.assertEqual(discovery["matched_patterns"], ["python -m pytest"]) def test_bilingual_authority_deletion_is_drift(self): with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) - config = copy.deepcopy(self.config("cml")) - repository_root = _materialize_repository(snapshot_root, config) + repository_root = _materialize_repository( + snapshot_root, copy.deepcopy(self.config("cml")) + ) contract = repository_root / "docs/LOTUS.md" contract.write_text( contract.read_text(encoding="utf-8").replace( @@ -110,130 +151,76 @@ def test_bilingual_authority_deletion_is_drift(self): ), encoding="utf-8", ) - result = self.audit("cml", snapshot_root) - self.assertEqual(result["outcome"], DRIFT) - self.assertEqual(result["reason_code"], "LOTUS_CONTRACT_DRIFT") - authority_check = next( + check = next( row for row in result["checks"] if row["check_id"] == "bilingual_contract" ) - self.assertIn( - "не имеет права собственности", - authority_check["missing_terms"], - ) + self.assertIn("не имеет права собственности", check["missing_terms"]) def test_contract_test_not_discovered_by_ci_is_drift(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - config = self.config("ls") - repository_root = _materialize_repository(snapshot_root, config) - workflow = repository_root / ".github/workflows/ci.yml" - workflow.write_text("python -m pytest tests/test_other.py\n", encoding="utf-8") - - result = self.audit("ls", snapshot_root) - - self.assertEqual(result["outcome"], DRIFT) - discovery = next( - row for row in result["checks"] if row["check_id"] == "ci_discovery" - ) - self.assertEqual(discovery["outcome"], DRIFT) - self.assertEqual(discovery["matched_patterns"], []) + self.assert_ci_drift("ls", "python -m pytest tests/test_other.py\n") def test_cml_unrelated_pytest_subset_is_not_full_discovery(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - config = self.config("cml") - repository_root = _materialize_repository(snapshot_root, config) - workflow = repository_root / ".github/workflows/ci.yml" - workflow.write_text( - "python -m pytest tests/test_other.py\n", - encoding="utf-8", - ) - - result = self.audit("cml", snapshot_root) - - self.assertEqual(result["outcome"], DRIFT) - discovery = next( - row for row in result["checks"] if row["check_id"] == "ci_discovery" - ) - self.assertEqual(discovery["outcome"], DRIFT) - self.assertEqual(discovery["matched_patterns"], []) + self.assert_ci_drift("cml", "python -m pytest tests/test_other.py\n") def test_cml_pytest_ignore_of_contract_test_is_drift(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - config = self.config("cml") - repository_root = _materialize_repository(snapshot_root, config) - workflow = repository_root / ".github/workflows/ci.yml" - workflow.write_text( - "python -m pytest --ignore=tests/test_lotus_docs_contract.py\n", - encoding="utf-8", - ) + self.assert_ci_drift( + "cml", + "python -m pytest --ignore=tests/test_lotus_docs_contract.py\n", + ) - result = self.audit("cml", snapshot_root) + def test_cml_commented_pytest_command_is_drift(self): + self.assert_ci_drift("cml", "# python -m pytest\n") - self.assertEqual(result["outcome"], DRIFT) - discovery = next( - row for row in result["checks"] if row["check_id"] == "ci_discovery" - ) - self.assertEqual(discovery["outcome"], DRIFT) + def test_cml_pytest_ignore_directory_covering_contract_test_is_drift(self): + self.assert_ci_drift("cml", "python -m pytest --ignore=tests\n") - def test_cml_commented_pytest_command_is_drift(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - config = self.config("cml") - repository_root = _materialize_repository(snapshot_root, config) - workflow = repository_root / ".github/workflows/ci.yml" - workflow.write_text("# python -m pytest\n", encoding="utf-8") + def test_cml_pytest_ignore_glob_covering_contract_test_is_drift(self): + self.assert_ci_drift( + "cml", "python -m pytest --ignore-glob='tests/*'\n" + ) - result = self.audit("cml", snapshot_root) + def test_cml_pytest_ignore_glob_with_dot_prefix_is_drift(self): + self.assert_ci_drift( + "cml", "python -m pytest --ignore-glob='./tests/*'\n" + ) - self.assertEqual(result["outcome"], DRIFT) - discovery = next( - row for row in result["checks"] if row["check_id"] == "ci_discovery" - ) - self.assertEqual(discovery["matched_patterns"], []) + def test_cml_echo_pytest_is_not_executed(self): + self.assert_ci_drift("cml", "echo python -m pytest\n") - def test_cml_pytest_ignore_directory_covering_contract_test_is_drift(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - config = self.config("cml") - repository_root = _materialize_repository(snapshot_root, config) - workflow = repository_root / ".github/workflows/ci.yml" - workflow.write_text( - "python -m pytest --ignore=tests\n", - encoding="utf-8", - ) + def test_cml_false_and_pytest_is_not_executed(self): + self.assert_ci_drift("cml", "false && python -m pytest\n") - result = self.audit("cml", snapshot_root) + def test_cml_pytest_k_filter_is_drift(self): + self.assert_ci_drift( + "cml", "python -m pytest -k='not lotus_docs_contract'\n" + ) - self.assertEqual(result["outcome"], DRIFT) - discovery = next( - row for row in result["checks"] if row["check_id"] == "ci_discovery" - ) - self.assertEqual(discovery["matched_patterns"], []) + def test_ls_commented_contract_test_is_drift(self): + self.assert_ci_drift( + "ls", "# python -m pytest tests/test_lotus_docs_contract.py\n" + ) - def test_cml_pytest_ignore_glob_covering_contract_test_is_drift(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - config = self.config("cml") - repository_root = _materialize_repository(snapshot_root, config) - workflow = repository_root / ".github/workflows/ci.yml" - workflow.write_text( - "python -m pytest --ignore-glob='tests/*'\n", - encoding="utf-8", - ) + def test_ls_ignored_contract_test_is_drift(self): + self.assert_ci_drift( + "ls", + "python -m pytest --ignore=tests/test_lotus_docs_contract.py " + "tests/test_lotus_docs_contract.py\n", + ) - result = self.audit("cml", snapshot_root) + def test_ls_echo_contract_test_is_drift(self): + self.assert_ci_drift( + "ls", "echo python -m pytest tests/test_lotus_docs_contract.py\n" + ) - self.assertEqual(result["outcome"], DRIFT) - discovery = next( - row for row in result["checks"] if row["check_id"] == "ci_discovery" - ) - self.assertEqual(discovery["matched_patterns"], []) + def test_pythia_commented_mix_test_is_drift(self): + self.assert_ci_drift("pythia", "# mix test\n") + + def test_pythia_echo_mix_test_is_drift(self): + self.assert_ci_drift("pythia", "echo mix test\n") def test_empty_manifest_term_list_is_unknown_not_pass(self): with tempfile.TemporaryDirectory() as directory: @@ -241,7 +228,6 @@ def test_empty_manifest_term_list_is_unknown_not_pass(self): _materialize_repository(snapshot_root, self.config("pythia")) manifest = copy.deepcopy(self.manifest) manifest["repositories"][0]["file_checks"][0]["contains_all"] = [] - result = audit_repository( manifest, repository_id="pythia", @@ -249,7 +235,6 @@ def test_empty_manifest_term_list_is_unknown_not_pass(self): repository_ref="refs/heads/main", commit_sha=SHA, ) - self.assertEqual( (result["outcome"], result["reason_code"]), (UNKNOWN, "MANIFEST_INVALID"), @@ -259,11 +244,13 @@ def test_manifest_path_traversal_is_unknown_and_not_hashed(self): with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) _materialize_repository(snapshot_root, self.config("pythia")) - outside = snapshot_root / "outside.md" - outside.write_text("all required terms\n", encoding="utf-8") + (snapshot_root / "outside.md").write_text( + "all required terms\n", encoding="utf-8" + ) manifest = copy.deepcopy(self.manifest) - manifest["repositories"][0]["file_checks"][0]["path"] = "../outside.md" - + manifest["repositories"][0]["file_checks"][0]["path"] = ( + "../outside.md" + ) result = audit_repository( manifest, repository_id="pythia", @@ -271,7 +258,6 @@ def test_manifest_path_traversal_is_unknown_and_not_hashed(self): repository_ref="refs/heads/main", commit_sha=SHA, ) - self.assertEqual( (result["outcome"], result["reason_code"]), (UNKNOWN, "MANIFEST_INVALID"), @@ -281,7 +267,6 @@ def test_manifest_path_traversal_is_unknown_and_not_hashed(self): def test_missing_repository_snapshot_is_unknown_not_pass(self): with tempfile.TemporaryDirectory() as directory: result = self.audit("pythia", Path(directory)) - self.assertEqual( (result["outcome"], result["reason_code"]), (UNKNOWN, "SNAPSHOT_UNAVAILABLE"), @@ -291,7 +276,6 @@ def test_invalid_commit_sha_is_unknown_not_pass(self): with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) _materialize_repository(snapshot_root, self.config("pythia")) - result = audit_repository( self.manifest, repository_id="pythia", @@ -299,7 +283,6 @@ def test_invalid_commit_sha_is_unknown_not_pass(self): repository_ref="refs/heads/main", commit_sha="main", ) - self.assertEqual( (result["outcome"], result["reason_code"]), (UNKNOWN, "COMMIT_SHA_INVALID"), @@ -318,12 +301,15 @@ def test_file_hash_changes_when_checked_content_changes(self): encoding="utf-8", ) second = self.audit("pythia", snapshot_root) - first_hash = next( - row["sha256"] for row in first["files"] if row["path"] == "LOTUS.md" + row["sha256"] + for row in first["files"] + if row["path"] == "LOTUS.md" ) second_hash = next( - row["sha256"] for row in second["files"] if row["path"] == "LOTUS.md" + row["sha256"] + for row in second["files"] + if row["path"] == "LOTUS.md" ) self.assertNotEqual(first_hash, second_hash) From 24d7e12228f8439aa7cd1dab3a80f8af5755d060 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:11:25 +0300 Subject: [PATCH 24/79] fix: harden Lotus workflow command discovery --- .../conformance/lotus_family_auditor.py | 909 ++++++------------ .../conformance/lotus_family_auditor_core.py | 707 ++++++++++++++ .../lotus_family_auditor_legacy_tests.py | 318 ++++++ .../conformance/test_lotus_family_auditor.py | 341 ++----- 4 files changed, 1360 insertions(+), 915 deletions(-) create mode 100644 standards/lotus-family/conformance/lotus_family_auditor_core.py create mode 100644 standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py diff --git a/standards/lotus-family/conformance/lotus_family_auditor.py b/standards/lotus-family/conformance/lotus_family_auditor.py index 44d528d0..540a2683 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor.py +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -1,273 +1,20 @@ from __future__ import annotations -import argparse -import fnmatch -import hashlib import json import re import shlex -from pathlib import Path, PurePosixPath from typing import Any, Mapping -PASS = "PASS" -DRIFT = "DRIFT" -UNKNOWN = "UNKNOWN" - -_COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") -_PYTEST_STARTS = (("python", "-m", "pytest"), ("pytest",)) -_SAFE_PYTEST_FLAGS = { - "-q", "--quiet", "-v", "--verbose", "--strict-markers", - "--strict-config", "--disable-warnings", -} -_SAFE_PYTEST_PREFIXES = ( - "--junitxml=", "--cov=", "--cov-report=", "--color=", "--tb=", - "--durations=", "--maxfail=", -) -_FORBIDDEN_PYTEST_FLAGS = { - "--collect-only", "--co", "--setup-only", "--pyargs", - "-k", "--keyword", "-m", "--markers", "--deselect", -} -_SHELL_CONTROL = {"&&", "||", ";", "|", "&"} - - -def load_json_object(path: Path) -> dict[str, Any]: - value = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(value, dict): - raise ValueError(f"{path} root must be an object") - return value - - -def _non_empty_string(value: Any, field: str) -> str: - if not isinstance(value, str) or not value.strip(): - raise ValueError(f"{field} must be a non-empty string") - return value - - -def _relative_manifest_path(value: Any, field: str) -> str: - text = _non_empty_string(value, field) - if "\\" in text: - raise ValueError(f"{field} must use repository-style '/' separators") - path = PurePosixPath(text) - if path.is_absolute() or path == PurePosixPath(".") or ".." in path.parts: - raise ValueError(f"{field} must stay inside the repository snapshot") - return path.as_posix() - - -def _validate_manifest(manifest: Mapping[str, Any]) -> None: - if manifest.get("schema_version") != "pythia.lotus_family_manifest.v0.1": - raise ValueError("unsupported Lotus Family manifest schema") - if manifest.get("authority") != "audit_only": - raise ValueError("Lotus Family manifest authority must be audit_only") - repositories = manifest.get("repositories") - if not isinstance(repositories, list) or not repositories: - raise ValueError("manifest repositories must be a non-empty list") - - ids: set[str] = set() - dirs: set[str] = set() - for repo_index, repo in enumerate(repositories): - prefix = f"repositories[{repo_index}]" - if not isinstance(repo, Mapping): - raise ValueError(f"{prefix} must be an object") - repo_id = _non_empty_string(repo.get("id"), f"{prefix}.id") - if repo_id in ids: - raise ValueError(f"duplicate repository id: {repo_id}") - ids.add(repo_id) - _non_empty_string(repo.get("repository"), f"{prefix}.repository") - snapshot_dir = _relative_manifest_path( - repo.get("snapshot_dir"), f"{prefix}.snapshot_dir" - ) - if "/" in snapshot_dir: - raise ValueError(f"{prefix}.snapshot_dir must be one directory name") - if snapshot_dir in dirs: - raise ValueError(f"duplicate snapshot_dir: {snapshot_dir}") - dirs.add(snapshot_dir) - - checks = repo.get("file_checks") - if not isinstance(checks, list) or not checks: - raise ValueError(f"{prefix}.file_checks must be a non-empty list") - check_ids: set[str] = set() - checked_paths: set[str] = set() - for check_index, check in enumerate(checks): - check_prefix = f"{prefix}.file_checks[{check_index}]" - if not isinstance(check, Mapping): - raise ValueError(f"{check_prefix} must be an object") - check_id = _non_empty_string(check.get("id"), f"{check_prefix}.id") - if check_id in check_ids: - raise ValueError(f"duplicate check id in {repo_id}: {check_id}") - check_ids.add(check_id) - checked_paths.add( - _relative_manifest_path(check.get("path"), f"{check_prefix}.path") - ) - terms = check.get("contains_all") - if not isinstance(terms, list) or not terms: - raise ValueError( - f"{check_prefix}.contains_all must be a non-empty list" - ) - for term_index, term in enumerate(terms): - _non_empty_string( - term, f"{check_prefix}.contains_all[{term_index}]" - ) - - discovery = repo.get("ci_discovery") - if not isinstance(discovery, Mapping): - raise ValueError(f"{prefix}.ci_discovery must be an object") - workflows = discovery.get("workflow_paths") - if not isinstance(workflows, list) or not workflows: - raise ValueError( - f"{prefix}.ci_discovery.workflow_paths must be a non-empty list" - ) - for path_index, workflow_path in enumerate(workflows): - _relative_manifest_path( - workflow_path, - f"{prefix}.ci_discovery.workflow_paths[{path_index}]", - ) - - strategy = discovery.get("strategy") - if strategy == "contains_any": - patterns = discovery.get("contains_any") - if not isinstance(patterns, list) or not patterns: - raise ValueError( - f"{prefix}.ci_discovery.contains_any must be a non-empty list" - ) - for pattern_index, pattern in enumerate(patterns): - _non_empty_string( - pattern, - f"{prefix}.ci_discovery.contains_any[{pattern_index}]", - ) - elif strategy == "pytest_default_discovery": - command = _non_empty_string( - discovery.get("command"), f"{prefix}.ci_discovery.command" - ) - if command != "python -m pytest": - raise ValueError( - f"{prefix}.ci_discovery.command must be 'python -m pytest'" - ) - test_path = _relative_manifest_path( - discovery.get("test_path"), f"{prefix}.ci_discovery.test_path" - ) - test_name = PurePosixPath(test_path).name - if not (test_name.startswith("test_") and test_name.endswith(".py")): - raise ValueError( - f"{prefix}.ci_discovery.test_path is not pytest-discoverable" - ) - if test_path not in checked_paths: - raise ValueError( - f"{prefix}.ci_discovery.test_path must also be a checked file" - ) - else: - raise ValueError( - f"{prefix}.ci_discovery.strategy is unsupported: {strategy}" - ) - - -def load_manifest(path: Path) -> dict[str, Any]: - manifest = load_json_object(path) - _validate_manifest(manifest) - return manifest - - -def _sha256(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def _result( - *, - outcome: str, - reason_code: str, - detail: str, - repository: str, - repository_id: str, - repository_ref: str, - commit_sha: str, - manifest_schema: str, - checks: list[dict[str, Any]], - files: list[dict[str, str]], -) -> dict[str, Any]: - return { - "schema_version": "pythia.lotus_family_audit_result.v0.1", - "outcome": outcome, - "reason_code": reason_code, - "detail": detail, - "repository": repository, - "repository_id": repository_id, - "repository_ref": repository_ref, - "commit_sha": commit_sha, - "manifest_schema": manifest_schema, - "checks": checks, - "files": sorted(files, key=lambda row: row["path"]), - "authority": { - "mode": "audit_only", - "grants_ownership": False, - "grants_approval": False, - "grants_execution": False, - "grants_delivery": False, - "grants_merge": False, - }, - } - +import lotus_family_auditor_core as _core +from lotus_family_auditor_core import * # noqa: F403 -def _manifest_invalid_result( - detail: str, - repository_id: str, - repository_ref: str, - commit_sha: str, - manifest_schema: str, -) -> dict[str, Any]: - return _result( - outcome=UNKNOWN, - reason_code="MANIFEST_INVALID", - detail=detail, - repository="", - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - - -def _repository_config( - manifest: Mapping[str, Any], repository_id: str -) -> Mapping[str, Any] | None: - return next( - (row for row in manifest["repositories"] if row.get("id") == repository_id), - None, - ) - - -def _contained_path( - root: Path, relative_path: str -) -> tuple[Path | None, str | None]: - try: - normalized = _relative_manifest_path(relative_path, "manifest path") - root_resolved = root.resolve() - path = (root_resolved / Path(*PurePosixPath(normalized).parts)).resolve() - path.relative_to(root_resolved) - return path, None - except (OSError, ValueError): - return None, f"path escapes repository snapshot: {relative_path}" - - -def _read_file( - root: Path, - relative_path: str, - files: list[dict[str, str]], -) -> tuple[str | None, str | None]: - path, error = _contained_path(root, relative_path) - if error is not None or path is None: - return None, error - if not path.is_file(): - return None, f"required file is missing: {relative_path}" - try: - text = path.read_text(encoding="utf-8") - files.append({"path": relative_path, "sha256": _sha256(path)}) - return text, None - except (OSError, UnicodeError) as exc: - return None, f"cannot read {relative_path}: {exc.__class__.__name__}" +_YAML_KEY = re.compile(r"([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)\Z") +_BLOCK_SCALAR = re.compile(r"([|>])(?:[1-9][+-]?|[+-][1-9]?)?\Z") +_ENV_ASSIGNMENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*=.*\Z") +_SHELL_CONTROL = {"&&", "||", ";", "|", "&"} -def _strip_unquoted_shell_comment(line: str) -> str: +def _strip_unquoted_comment(line: str) -> str: in_single = False in_double = False escaped = False @@ -289,170 +36,300 @@ def _strip_unquoted_shell_comment(line: str) -> str: return line -def _shell_commands(text: str) -> list[str]: - lines = text.splitlines() - commands: list[str] = [] - index = 0 - while index < len(lines): - first = _strip_unquoted_shell_comment(lines[index]).strip() - if not first: - index += 1 - continue - parts = [first] - while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): - parts[-1] = parts[-1].rstrip()[:-1] - index += 1 - continuation = _strip_unquoted_shell_comment(lines[index]).strip() - if continuation: - parts.append(continuation) - commands.append(" ".join(parts)) - index += 1 - return commands +def _indent_of(line: str) -> int | None: + prefix = line[: len(line) - len(line.lstrip(" \t"))] + if "\t" in prefix: + return None + return len(prefix) -def _tokenize_executed_command(command: str) -> list[str] | None: - try: - tokens = shlex.split(command) - except ValueError: +def _yaml_header(line: str) -> tuple[int, bool, int, str, str] | None: + indent = _indent_of(line) + if indent is None: return None - if not tokens or any(token in _SHELL_CONTROL for token in tokens): + stripped = line[indent:] + if not stripped or stripped.startswith("#"): return None - index = 0 - while index < len(tokens) and re.fullmatch( - r"[A-Za-z_][A-Za-z0-9_]*=.*", tokens[index] - ): - index += 1 - return tokens[index:] or None - -def _pytest_parts(tokens: list[str]) -> tuple[list[str], list[str]] | None: - for start in _PYTEST_STARTS: - if tuple(tokens[: len(start)]) == start: - return list(start), tokens[len(start) :] - return None - - -def _normalize_repo_pattern(value: str) -> str: - normalized = value.strip().replace("\\", "/") - while normalized.startswith("./"): - normalized = normalized[2:] - return normalized.rstrip("/") - - -def _ignore_path_covers_test(ignore_path: str, test_path: str) -> bool: - normalized = _normalize_repo_pattern(ignore_path) - if not normalized: - return True - ignored = PurePosixPath(normalized) - test = PurePosixPath(test_path) - if ignored.is_absolute() or ".." in ignored.parts: - return True - return ignored == test or ignored in test.parents + is_list = False + key_indent = indent + if stripped.startswith("-"): + match = re.match(r"-([ ]+)(.*)\Z", stripped) + if match is None: + return None + is_list = True + key_indent = indent + 1 + len(match.group(1)) + stripped = match.group(2) + match = _YAML_KEY.fullmatch(stripped) + if match is None: + return None + return indent, is_list, key_indent, match.group(1), match.group(2) -def _ignore_glob_covers_test(pattern: str, test_path: str) -> bool: - normalized = _normalize_repo_pattern(pattern) - if not normalized: - return True - test = PurePosixPath(test_path) - candidates = [test.as_posix(), test.name] - candidates.extend( - parent.as_posix() - for parent in test.parents - if parent != PurePosixPath(".") - ) - return any( - fnmatch.fnmatchcase(candidate, normalized) for candidate in candidates - ) +def _scalar_indicator(value: str) -> str | None: + normalized = _strip_unquoted_comment(value).strip() + match = _BLOCK_SCALAR.fullmatch(normalized) + return match.group(1) if match is not None else None -def _pytest_arguments_safe( - arguments: list[str], test_path: str, *, require_test_path: bool -) -> bool: - positive_paths: list[str] = [] +def _block_scalar_end(lines: list[str], start: int, key_indent: int) -> int: + index = start + 1 + while index < len(lines): + line = lines[index] + if not line.strip(): + index += 1 + continue + indent = _indent_of(line) + if indent is None or indent <= key_indent: + break + index += 1 + return index + + +def _block_scalar_text( + lines: list[str], start: int, end: int, style: str +) -> str: + body = lines[start + 1 : end] + indents = [ + indent + for line in body + if line.strip() and (indent := _indent_of(line)) is not None + ] + if not indents: + return "" + content_indent = min(indents) + deindented = [line[content_indent:] if line.strip() else "" for line in body] + if style == "|": + return "\n".join(deindented) + "\n" + + paragraphs: list[str] = [] + current: list[str] = [] + for line in deindented: + if line: + current.append(line) + elif current: + paragraphs.append(" ".join(current)) + current = [] + elif paragraphs: + paragraphs.append("") + if current: + paragraphs.append(" ".join(current)) + return "\n".join(paragraphs) + ("\n" if paragraphs else "") + + +def _inline_yaml_scalar(value: str) -> str | None: + text = _strip_unquoted_comment(value).strip() + if not text or text in {"null", "Null", "NULL", "~"}: + return None + if text.startswith("'"): + if len(text) < 2 or not text.endswith("'"): + return None + return text[1:-1].replace("''", "'") + if text.startswith('"'): + if len(text) < 2 or not text.endswith('"'): + return None + try: + decoded = json.loads(text) + except json.JSONDecodeError: + return None + return decoded if isinstance(decoded, str) else None + return text + + +def _scalar_ranges(lines: list[str]) -> dict[int, tuple[int, str]]: + ranges: dict[int, tuple[int, str]] = {} index = 0 - while index < len(arguments): - argument = arguments[index] - if argument in _FORBIDDEN_PYTEST_FLAGS or argument.startswith( - ("-k=", "--keyword=", "-m=", "--markers=", "--deselect=") - ): - return False - if argument.startswith("--ignore="): - if _ignore_path_covers_test(argument.split("=", 1)[1], test_path): - return False - elif argument == "--ignore": - if index + 1 >= len(arguments): - return False - if _ignore_path_covers_test(arguments[index + 1], test_path): - return False + while index < len(lines): + header = _yaml_header(lines[index]) + if header is None: index += 1 - elif argument.startswith("--ignore-glob="): - if _ignore_glob_covers_test(argument.split("=", 1)[1], test_path): - return False - elif argument == "--ignore-glob": - if index + 1 >= len(arguments): - return False - if _ignore_glob_covers_test(arguments[index + 1], test_path): - return False + continue + style = _scalar_indicator(header[4]) + if style is None: index += 1 - elif argument in _SAFE_PYTEST_FLAGS or argument.startswith( - _SAFE_PYTEST_PREFIXES - ): - pass - elif argument.startswith("-"): - return False - else: - positive_paths.append( - _normalize_repo_pattern(argument.split("::", 1)[0]) - ) - index += 1 - - normalized_test = _normalize_repo_pattern(test_path) - if require_test_path: - return normalized_test in positive_paths - return not positive_paths + continue + end = _block_scalar_end(lines, index, header[2]) + ranges[index] = (end, style) + index = end + return ranges -def _is_pytest_default_discovery(command: str, test_path: str) -> bool: - tokens = _tokenize_executed_command(command) - if tokens is None: - return False - parts = _pytest_parts(tokens) - return parts is not None and _pytest_arguments_safe( - parts[1], test_path, require_test_path=False - ) +def _github_actions_run_scripts(text: str) -> list[str]: + lines = text.splitlines() + scalar_ranges = _scalar_ranges(lines) + scalar_body_lines = { + line_index + for start, (end, _) in scalar_ranges.items() + for line_index in range(start + 1, end) + } + scripts: list[str] = [] + index = 0 + while index < len(lines): + if index in scalar_body_lines: + index += 1 + continue + header = _yaml_header(lines[index]) + if ( + header is None + or header[3] != "steps" + or _inline_yaml_scalar(header[4]) is not None + ): + index += 1 + continue -def _is_explicit_pytest_test_command(command: str, test_path: str) -> bool: - tokens = _tokenize_executed_command(command) - if tokens is None: - return False - parts = _pytest_parts(tokens) - return parts is not None and _pytest_arguments_safe( - parts[1], test_path, require_test_path=True - ) + steps_indent = header[2] + block_end = index + 1 + while block_end < len(lines): + line = lines[block_end] + if not line.strip() or line.lstrip().startswith("#"): + block_end += 1 + continue + indent = _indent_of(line) + if indent is None or indent <= steps_indent: + break + block_end += 1 + + item_indices: list[int] = [] + item_indent: int | None = None + for cursor in range(index + 1, block_end): + if cursor in scalar_body_lines: + continue + indent = _indent_of(lines[cursor]) + stripped = lines[cursor][indent:] if indent is not None else "" + if indent is not None and stripped.startswith("-"): + if item_indent is None: + item_indent = indent + if indent == item_indent: + item_indices.append(cursor) + + for position, item_start in enumerate(item_indices): + item_end = ( + item_indices[position + 1] + if position + 1 < len(item_indices) + else block_end + ) + item_header = _yaml_header(lines[item_start]) + if item_header is not None and item_header[1]: + direct_indent = item_header[2] + else: + candidates = [ + candidate[2] + for row in range(item_start + 1, item_end) + if row not in scalar_body_lines + and (candidate := _yaml_header(lines[row])) is not None + and not candidate[1] + ] + if not candidates: + continue + direct_indent = min(candidates) + + for row in range(item_start, item_end): + if row in scalar_body_lines: + continue + row_header = _yaml_header(lines[row]) + if row_header is None: + continue + _, is_list, key_indent, key, value = row_header + if key != "run" or key_indent != direct_indent: + continue + if row != item_start and is_list: + continue + scalar = scalar_ranges.get(row) + if scalar is not None: + end, style = scalar + scripts.append(_block_scalar_text(lines, row, end, style)) + else: + inline = _inline_yaml_scalar(value) + if inline is not None: + scripts.append(inline) + break + index = block_end + return scripts + + +def _yaml_env_names(text: str) -> set[str]: + lines = text.splitlines() + scalar_ranges = _scalar_ranges(lines) + scalar_body_lines = { + line_index + for start, (end, _) in scalar_ranges.items() + for line_index in range(start + 1, end) + } + names: set[str] = set() + for index, line in enumerate(lines): + if index in scalar_body_lines: + continue + header = _yaml_header(line) + if header is None or header[3] != "env": + continue + env_indent = header[2] + inline = _strip_unquoted_comment(header[4]).strip() + if inline: + names.update(re.findall(r"\bPYTEST_[A-Za-z0-9_]*\b", inline)) + continue + for cursor in range(index + 1, len(lines)): + if cursor in scalar_body_lines: + continue + candidate = lines[cursor] + if not candidate.strip() or candidate.lstrip().startswith("#"): + continue + candidate_indent = _indent_of(candidate) + if candidate_indent is None or candidate_indent <= env_indent: + break + child = _yaml_header(candidate) + if child is not None and not child[1] and child[2] > env_indent: + names.add(child[3]) + return names + + +def _has_forbidden_pytest_environment(text: str, scripts: list[str]) -> bool: + if any(name.startswith("PYTEST_") for name in _yaml_env_names(text)): + return True + for script in scripts: + executable = "\n".join( + _strip_unquoted_comment(line) for line in script.splitlines() + ) + if re.search(r"\bPYTEST_[A-Za-z0-9_]*\b", executable): + return True + return False -def _is_command_prefix(command: str, prefix: str) -> bool: - tokens = _tokenize_executed_command(command) - if tokens is None: - return False +def _tokenize_executed_command(command: str) -> list[str] | None: try: - required = shlex.split(prefix) + lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") + lexer.whitespace_split = True + lexer.commenters = "" + tokens = list(lexer) except ValueError: - return False - return tokens[: len(required)] == required + return None + if not tokens or any(token in _SHELL_CONTROL for token in tokens): + return None + if _ENV_ASSIGNMENT.fullmatch(tokens[0]): + return None + return tokens def _ci_discovery( - discovery: Mapping[str, Any], combined: str + discovery: Mapping[str, Any], workflow_text: str | list[str] ) -> tuple[bool, list[str]]: - commands = _shell_commands(combined) + if isinstance(workflow_text, list): + workflow_text = "\n".join(workflow_text) + scripts = _github_actions_run_scripts(workflow_text) + commands = [ + command for script in scripts for command in _core._shell_commands(script) + ] strategy = discovery["strategy"] + uses_pytest = strategy == "pytest_default_discovery" or any( + str(pattern).endswith(".py") for pattern in discovery.get("contains_any", []) + ) + if uses_pytest and _has_forbidden_pytest_environment(workflow_text, scripts): + return False, [] + if strategy == "pytest_default_discovery": test_path = str(discovery["test_path"]) matched = any( - _is_pytest_default_discovery(command, test_path) + _core._is_pytest_default_discovery(command, test_path) for command in commands ) return matched, [str(discovery["command"])] if matched else [] @@ -462,246 +339,20 @@ def _ci_discovery( pattern = str(pattern_value) if pattern.endswith(".py"): if any( - _is_explicit_pytest_test_command(command, pattern) + _core._is_explicit_pytest_test_command(command, pattern) for command in commands ): matches.append(pattern) - elif any(_is_command_prefix(command, pattern) for command in commands): + elif any(_core._is_command_prefix(command, pattern) for command in commands): matches.append(pattern) return bool(matches), matches -def audit_repository( - manifest: Mapping[str, Any], - *, - repository_id: str, - snapshot_root: Path, - repository_ref: str, - commit_sha: str, -) -> dict[str, Any]: - manifest_schema = str(manifest.get("schema_version", "")) - try: - _validate_manifest(manifest) - except (TypeError, ValueError) as exc: - return _manifest_invalid_result( - str(exc), - repository_id, - repository_ref, - commit_sha, - manifest_schema, - ) - - config = _repository_config(manifest, repository_id) - if config is None: - return _result( - outcome=UNKNOWN, - reason_code="REPOSITORY_NOT_CONFIGURED", - detail=f"repository adapter is not configured: {repository_id}", - repository="", - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - - repository = str(config["repository"]) - if not repository_ref: - return _result( - outcome=UNKNOWN, - reason_code="REPOSITORY_REF_MISSING", - detail="an exact repository ref is required", - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - if not _COMMIT_SHA.fullmatch(commit_sha): - return _result( - outcome=UNKNOWN, - reason_code="COMMIT_SHA_INVALID", - detail="commit_sha must be a lowercase 40-character hexadecimal SHA", - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - - snapshot_root_resolved = snapshot_root.resolve() - repository_root = ( - snapshot_root_resolved / str(config["snapshot_dir"]) - ).resolve() - try: - repository_root.relative_to(snapshot_root_resolved) - except ValueError: - return _result( - outcome=UNKNOWN, - reason_code="SNAPSHOT_UNAVAILABLE", - detail="repository snapshot escapes snapshot_root", - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - if not repository_root.is_dir(): - return _result( - outcome=UNKNOWN, - reason_code="SNAPSHOT_UNAVAILABLE", - detail=f"repository snapshot is unavailable: {config['snapshot_dir']}", - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - - checks: list[dict[str, Any]] = [] - files: list[dict[str, str]] = [] - drift: list[str] = [] - for check in config["file_checks"]: - check_id = str(check["id"]) - relative_path = str(check["path"]) - text, error = _read_file(repository_root, relative_path, files) - if error is not None or text is None: - checks.append( - { - "check_id": check_id, - "outcome": DRIFT, - "path": relative_path, - "missing_terms": [], - "detail": error, - } - ) - drift.append(check_id) - continue - terms = [str(term) for term in check["contains_all"]] - missing_terms = [term for term in terms if term not in text] - outcome = PASS if not missing_terms else DRIFT - checks.append( - { - "check_id": check_id, - "outcome": outcome, - "path": relative_path, - "missing_terms": missing_terms, - "detail": ( - "all required terms are present" - if outcome == PASS - else "required terms are missing" - ), - } - ) - if outcome == DRIFT: - drift.append(check_id) - - discovery = config["ci_discovery"] - workflow_paths = [str(path) for path in discovery["workflow_paths"]] - workflow_texts: list[str] = [] - read_errors: list[str] = [] - for relative_path in workflow_paths: - text, error = _read_file(repository_root, relative_path, files) - if error is not None or text is None: - read_errors.append(error or f"cannot read {relative_path}") - else: - workflow_texts.append(text) - - discovered, matched_patterns = _ci_discovery( - discovery, "\n".join(workflow_texts) - ) - if read_errors and not workflow_texts: - ci_outcome = DRIFT - detail = "; ".join(read_errors) - elif discovered: - ci_outcome = PASS - detail = "contract regression test is discovered by executable CI" - else: - ci_outcome = DRIFT - detail = "no configured executable CI discovery rule matched" - if ci_outcome == DRIFT: - drift.append("ci_discovery") - checks.append( - { - "check_id": "ci_discovery", - "outcome": ci_outcome, - "paths": workflow_paths, - "matched_patterns": matched_patterns, - "detail": detail, - } - ) - - if drift: - outcome = DRIFT - reason_code = "LOTUS_CONTRACT_DRIFT" - detail = "non-conforming checks: " + ", ".join(sorted(set(drift))) - else: - outcome = PASS - reason_code = "LOTUS_CONTRACT_CONFORMANT" - detail = "all configured Lotus Family invariants passed" - - unique_files = {(row["path"], row["sha256"]): row for row in files} - return _result( - outcome=outcome, - reason_code=reason_code, - detail=detail, - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=checks, - files=list(unique_files.values()), - ) - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser( - description="Audit one exact repository snapshot against the Lotus Family manifest." - ) - parser.add_argument("--manifest", type=Path, required=True) - parser.add_argument("--snapshot-root", type=Path, required=True) - parser.add_argument("--repository-id", required=True) - parser.add_argument("--repository-ref", required=True) - parser.add_argument("--commit-sha", required=True) - parser.add_argument("--output", type=Path, required=True) - args = parser.parse_args(argv) - - try: - manifest = load_manifest(args.manifest) - result = audit_repository( - manifest, - repository_id=args.repository_id, - snapshot_root=args.snapshot_root, - repository_ref=args.repository_ref, - commit_sha=args.commit_sha, - ) - except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: - result = _manifest_invalid_result( - str(exc), - args.repository_id, - args.repository_ref, - args.commit_sha, - "", - ) - - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text( - json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - print(json.dumps(result, ensure_ascii=False, sort_keys=True)) - return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(result["outcome"], 3) +_core._tokenize_executed_command = _tokenize_executed_command +_core._ci_discovery = _ci_discovery +if "--cov-fail-under=" not in _core._SAFE_PYTEST_PREFIXES: + _core._SAFE_PYTEST_PREFIXES += ("--cov-fail-under=",) if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(_core.main()) diff --git a/standards/lotus-family/conformance/lotus_family_auditor_core.py b/standards/lotus-family/conformance/lotus_family_auditor_core.py new file mode 100644 index 00000000..44d528d0 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_auditor_core.py @@ -0,0 +1,707 @@ +from __future__ import annotations + +import argparse +import fnmatch +import hashlib +import json +import re +import shlex +from pathlib import Path, PurePosixPath +from typing import Any, Mapping + +PASS = "PASS" +DRIFT = "DRIFT" +UNKNOWN = "UNKNOWN" + +_COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") +_PYTEST_STARTS = (("python", "-m", "pytest"), ("pytest",)) +_SAFE_PYTEST_FLAGS = { + "-q", "--quiet", "-v", "--verbose", "--strict-markers", + "--strict-config", "--disable-warnings", +} +_SAFE_PYTEST_PREFIXES = ( + "--junitxml=", "--cov=", "--cov-report=", "--color=", "--tb=", + "--durations=", "--maxfail=", +) +_FORBIDDEN_PYTEST_FLAGS = { + "--collect-only", "--co", "--setup-only", "--pyargs", + "-k", "--keyword", "-m", "--markers", "--deselect", +} +_SHELL_CONTROL = {"&&", "||", ";", "|", "&"} + + +def load_json_object(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"{path} root must be an object") + return value + + +def _non_empty_string(value: Any, field: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field} must be a non-empty string") + return value + + +def _relative_manifest_path(value: Any, field: str) -> str: + text = _non_empty_string(value, field) + if "\\" in text: + raise ValueError(f"{field} must use repository-style '/' separators") + path = PurePosixPath(text) + if path.is_absolute() or path == PurePosixPath(".") or ".." in path.parts: + raise ValueError(f"{field} must stay inside the repository snapshot") + return path.as_posix() + + +def _validate_manifest(manifest: Mapping[str, Any]) -> None: + if manifest.get("schema_version") != "pythia.lotus_family_manifest.v0.1": + raise ValueError("unsupported Lotus Family manifest schema") + if manifest.get("authority") != "audit_only": + raise ValueError("Lotus Family manifest authority must be audit_only") + repositories = manifest.get("repositories") + if not isinstance(repositories, list) or not repositories: + raise ValueError("manifest repositories must be a non-empty list") + + ids: set[str] = set() + dirs: set[str] = set() + for repo_index, repo in enumerate(repositories): + prefix = f"repositories[{repo_index}]" + if not isinstance(repo, Mapping): + raise ValueError(f"{prefix} must be an object") + repo_id = _non_empty_string(repo.get("id"), f"{prefix}.id") + if repo_id in ids: + raise ValueError(f"duplicate repository id: {repo_id}") + ids.add(repo_id) + _non_empty_string(repo.get("repository"), f"{prefix}.repository") + snapshot_dir = _relative_manifest_path( + repo.get("snapshot_dir"), f"{prefix}.snapshot_dir" + ) + if "/" in snapshot_dir: + raise ValueError(f"{prefix}.snapshot_dir must be one directory name") + if snapshot_dir in dirs: + raise ValueError(f"duplicate snapshot_dir: {snapshot_dir}") + dirs.add(snapshot_dir) + + checks = repo.get("file_checks") + if not isinstance(checks, list) or not checks: + raise ValueError(f"{prefix}.file_checks must be a non-empty list") + check_ids: set[str] = set() + checked_paths: set[str] = set() + for check_index, check in enumerate(checks): + check_prefix = f"{prefix}.file_checks[{check_index}]" + if not isinstance(check, Mapping): + raise ValueError(f"{check_prefix} must be an object") + check_id = _non_empty_string(check.get("id"), f"{check_prefix}.id") + if check_id in check_ids: + raise ValueError(f"duplicate check id in {repo_id}: {check_id}") + check_ids.add(check_id) + checked_paths.add( + _relative_manifest_path(check.get("path"), f"{check_prefix}.path") + ) + terms = check.get("contains_all") + if not isinstance(terms, list) or not terms: + raise ValueError( + f"{check_prefix}.contains_all must be a non-empty list" + ) + for term_index, term in enumerate(terms): + _non_empty_string( + term, f"{check_prefix}.contains_all[{term_index}]" + ) + + discovery = repo.get("ci_discovery") + if not isinstance(discovery, Mapping): + raise ValueError(f"{prefix}.ci_discovery must be an object") + workflows = discovery.get("workflow_paths") + if not isinstance(workflows, list) or not workflows: + raise ValueError( + f"{prefix}.ci_discovery.workflow_paths must be a non-empty list" + ) + for path_index, workflow_path in enumerate(workflows): + _relative_manifest_path( + workflow_path, + f"{prefix}.ci_discovery.workflow_paths[{path_index}]", + ) + + strategy = discovery.get("strategy") + if strategy == "contains_any": + patterns = discovery.get("contains_any") + if not isinstance(patterns, list) or not patterns: + raise ValueError( + f"{prefix}.ci_discovery.contains_any must be a non-empty list" + ) + for pattern_index, pattern in enumerate(patterns): + _non_empty_string( + pattern, + f"{prefix}.ci_discovery.contains_any[{pattern_index}]", + ) + elif strategy == "pytest_default_discovery": + command = _non_empty_string( + discovery.get("command"), f"{prefix}.ci_discovery.command" + ) + if command != "python -m pytest": + raise ValueError( + f"{prefix}.ci_discovery.command must be 'python -m pytest'" + ) + test_path = _relative_manifest_path( + discovery.get("test_path"), f"{prefix}.ci_discovery.test_path" + ) + test_name = PurePosixPath(test_path).name + if not (test_name.startswith("test_") and test_name.endswith(".py")): + raise ValueError( + f"{prefix}.ci_discovery.test_path is not pytest-discoverable" + ) + if test_path not in checked_paths: + raise ValueError( + f"{prefix}.ci_discovery.test_path must also be a checked file" + ) + else: + raise ValueError( + f"{prefix}.ci_discovery.strategy is unsupported: {strategy}" + ) + + +def load_manifest(path: Path) -> dict[str, Any]: + manifest = load_json_object(path) + _validate_manifest(manifest) + return manifest + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _result( + *, + outcome: str, + reason_code: str, + detail: str, + repository: str, + repository_id: str, + repository_ref: str, + commit_sha: str, + manifest_schema: str, + checks: list[dict[str, Any]], + files: list[dict[str, str]], +) -> dict[str, Any]: + return { + "schema_version": "pythia.lotus_family_audit_result.v0.1", + "outcome": outcome, + "reason_code": reason_code, + "detail": detail, + "repository": repository, + "repository_id": repository_id, + "repository_ref": repository_ref, + "commit_sha": commit_sha, + "manifest_schema": manifest_schema, + "checks": checks, + "files": sorted(files, key=lambda row: row["path"]), + "authority": { + "mode": "audit_only", + "grants_ownership": False, + "grants_approval": False, + "grants_execution": False, + "grants_delivery": False, + "grants_merge": False, + }, + } + + +def _manifest_invalid_result( + detail: str, + repository_id: str, + repository_ref: str, + commit_sha: str, + manifest_schema: str, +) -> dict[str, Any]: + return _result( + outcome=UNKNOWN, + reason_code="MANIFEST_INVALID", + detail=detail, + repository="", + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + + +def _repository_config( + manifest: Mapping[str, Any], repository_id: str +) -> Mapping[str, Any] | None: + return next( + (row for row in manifest["repositories"] if row.get("id") == repository_id), + None, + ) + + +def _contained_path( + root: Path, relative_path: str +) -> tuple[Path | None, str | None]: + try: + normalized = _relative_manifest_path(relative_path, "manifest path") + root_resolved = root.resolve() + path = (root_resolved / Path(*PurePosixPath(normalized).parts)).resolve() + path.relative_to(root_resolved) + return path, None + except (OSError, ValueError): + return None, f"path escapes repository snapshot: {relative_path}" + + +def _read_file( + root: Path, + relative_path: str, + files: list[dict[str, str]], +) -> tuple[str | None, str | None]: + path, error = _contained_path(root, relative_path) + if error is not None or path is None: + return None, error + if not path.is_file(): + return None, f"required file is missing: {relative_path}" + try: + text = path.read_text(encoding="utf-8") + files.append({"path": relative_path, "sha256": _sha256(path)}) + return text, None + except (OSError, UnicodeError) as exc: + return None, f"cannot read {relative_path}: {exc.__class__.__name__}" + + +def _strip_unquoted_shell_comment(line: str) -> str: + in_single = False + in_double = False + escaped = False + for index, character in enumerate(line): + if escaped: + escaped = False + continue + if character == "\\" and not in_single: + escaped = True + continue + if character == "'" and not in_double: + in_single = not in_single + continue + if character == '"' and not in_single: + in_double = not in_double + continue + if character == "#" and not in_single and not in_double: + return line[:index] + return line + + +def _shell_commands(text: str) -> list[str]: + lines = text.splitlines() + commands: list[str] = [] + index = 0 + while index < len(lines): + first = _strip_unquoted_shell_comment(lines[index]).strip() + if not first: + index += 1 + continue + parts = [first] + while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): + parts[-1] = parts[-1].rstrip()[:-1] + index += 1 + continuation = _strip_unquoted_shell_comment(lines[index]).strip() + if continuation: + parts.append(continuation) + commands.append(" ".join(parts)) + index += 1 + return commands + + +def _tokenize_executed_command(command: str) -> list[str] | None: + try: + tokens = shlex.split(command) + except ValueError: + return None + if not tokens or any(token in _SHELL_CONTROL for token in tokens): + return None + index = 0 + while index < len(tokens) and re.fullmatch( + r"[A-Za-z_][A-Za-z0-9_]*=.*", tokens[index] + ): + index += 1 + return tokens[index:] or None + + +def _pytest_parts(tokens: list[str]) -> tuple[list[str], list[str]] | None: + for start in _PYTEST_STARTS: + if tuple(tokens[: len(start)]) == start: + return list(start), tokens[len(start) :] + return None + + +def _normalize_repo_pattern(value: str) -> str: + normalized = value.strip().replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + return normalized.rstrip("/") + + +def _ignore_path_covers_test(ignore_path: str, test_path: str) -> bool: + normalized = _normalize_repo_pattern(ignore_path) + if not normalized: + return True + ignored = PurePosixPath(normalized) + test = PurePosixPath(test_path) + if ignored.is_absolute() or ".." in ignored.parts: + return True + return ignored == test or ignored in test.parents + + +def _ignore_glob_covers_test(pattern: str, test_path: str) -> bool: + normalized = _normalize_repo_pattern(pattern) + if not normalized: + return True + test = PurePosixPath(test_path) + candidates = [test.as_posix(), test.name] + candidates.extend( + parent.as_posix() + for parent in test.parents + if parent != PurePosixPath(".") + ) + return any( + fnmatch.fnmatchcase(candidate, normalized) for candidate in candidates + ) + + +def _pytest_arguments_safe( + arguments: list[str], test_path: str, *, require_test_path: bool +) -> bool: + positive_paths: list[str] = [] + index = 0 + while index < len(arguments): + argument = arguments[index] + if argument in _FORBIDDEN_PYTEST_FLAGS or argument.startswith( + ("-k=", "--keyword=", "-m=", "--markers=", "--deselect=") + ): + return False + if argument.startswith("--ignore="): + if _ignore_path_covers_test(argument.split("=", 1)[1], test_path): + return False + elif argument == "--ignore": + if index + 1 >= len(arguments): + return False + if _ignore_path_covers_test(arguments[index + 1], test_path): + return False + index += 1 + elif argument.startswith("--ignore-glob="): + if _ignore_glob_covers_test(argument.split("=", 1)[1], test_path): + return False + elif argument == "--ignore-glob": + if index + 1 >= len(arguments): + return False + if _ignore_glob_covers_test(arguments[index + 1], test_path): + return False + index += 1 + elif argument in _SAFE_PYTEST_FLAGS or argument.startswith( + _SAFE_PYTEST_PREFIXES + ): + pass + elif argument.startswith("-"): + return False + else: + positive_paths.append( + _normalize_repo_pattern(argument.split("::", 1)[0]) + ) + index += 1 + + normalized_test = _normalize_repo_pattern(test_path) + if require_test_path: + return normalized_test in positive_paths + return not positive_paths + + +def _is_pytest_default_discovery(command: str, test_path: str) -> bool: + tokens = _tokenize_executed_command(command) + if tokens is None: + return False + parts = _pytest_parts(tokens) + return parts is not None and _pytest_arguments_safe( + parts[1], test_path, require_test_path=False + ) + + +def _is_explicit_pytest_test_command(command: str, test_path: str) -> bool: + tokens = _tokenize_executed_command(command) + if tokens is None: + return False + parts = _pytest_parts(tokens) + return parts is not None and _pytest_arguments_safe( + parts[1], test_path, require_test_path=True + ) + + +def _is_command_prefix(command: str, prefix: str) -> bool: + tokens = _tokenize_executed_command(command) + if tokens is None: + return False + try: + required = shlex.split(prefix) + except ValueError: + return False + return tokens[: len(required)] == required + + +def _ci_discovery( + discovery: Mapping[str, Any], combined: str +) -> tuple[bool, list[str]]: + commands = _shell_commands(combined) + strategy = discovery["strategy"] + if strategy == "pytest_default_discovery": + test_path = str(discovery["test_path"]) + matched = any( + _is_pytest_default_discovery(command, test_path) + for command in commands + ) + return matched, [str(discovery["command"])] if matched else [] + + matches: list[str] = [] + for pattern_value in discovery["contains_any"]: + pattern = str(pattern_value) + if pattern.endswith(".py"): + if any( + _is_explicit_pytest_test_command(command, pattern) + for command in commands + ): + matches.append(pattern) + elif any(_is_command_prefix(command, pattern) for command in commands): + matches.append(pattern) + return bool(matches), matches + + +def audit_repository( + manifest: Mapping[str, Any], + *, + repository_id: str, + snapshot_root: Path, + repository_ref: str, + commit_sha: str, +) -> dict[str, Any]: + manifest_schema = str(manifest.get("schema_version", "")) + try: + _validate_manifest(manifest) + except (TypeError, ValueError) as exc: + return _manifest_invalid_result( + str(exc), + repository_id, + repository_ref, + commit_sha, + manifest_schema, + ) + + config = _repository_config(manifest, repository_id) + if config is None: + return _result( + outcome=UNKNOWN, + reason_code="REPOSITORY_NOT_CONFIGURED", + detail=f"repository adapter is not configured: {repository_id}", + repository="", + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + + repository = str(config["repository"]) + if not repository_ref: + return _result( + outcome=UNKNOWN, + reason_code="REPOSITORY_REF_MISSING", + detail="an exact repository ref is required", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + if not _COMMIT_SHA.fullmatch(commit_sha): + return _result( + outcome=UNKNOWN, + reason_code="COMMIT_SHA_INVALID", + detail="commit_sha must be a lowercase 40-character hexadecimal SHA", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + + snapshot_root_resolved = snapshot_root.resolve() + repository_root = ( + snapshot_root_resolved / str(config["snapshot_dir"]) + ).resolve() + try: + repository_root.relative_to(snapshot_root_resolved) + except ValueError: + return _result( + outcome=UNKNOWN, + reason_code="SNAPSHOT_UNAVAILABLE", + detail="repository snapshot escapes snapshot_root", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + if not repository_root.is_dir(): + return _result( + outcome=UNKNOWN, + reason_code="SNAPSHOT_UNAVAILABLE", + detail=f"repository snapshot is unavailable: {config['snapshot_dir']}", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=[], + files=[], + ) + + checks: list[dict[str, Any]] = [] + files: list[dict[str, str]] = [] + drift: list[str] = [] + for check in config["file_checks"]: + check_id = str(check["id"]) + relative_path = str(check["path"]) + text, error = _read_file(repository_root, relative_path, files) + if error is not None or text is None: + checks.append( + { + "check_id": check_id, + "outcome": DRIFT, + "path": relative_path, + "missing_terms": [], + "detail": error, + } + ) + drift.append(check_id) + continue + terms = [str(term) for term in check["contains_all"]] + missing_terms = [term for term in terms if term not in text] + outcome = PASS if not missing_terms else DRIFT + checks.append( + { + "check_id": check_id, + "outcome": outcome, + "path": relative_path, + "missing_terms": missing_terms, + "detail": ( + "all required terms are present" + if outcome == PASS + else "required terms are missing" + ), + } + ) + if outcome == DRIFT: + drift.append(check_id) + + discovery = config["ci_discovery"] + workflow_paths = [str(path) for path in discovery["workflow_paths"]] + workflow_texts: list[str] = [] + read_errors: list[str] = [] + for relative_path in workflow_paths: + text, error = _read_file(repository_root, relative_path, files) + if error is not None or text is None: + read_errors.append(error or f"cannot read {relative_path}") + else: + workflow_texts.append(text) + + discovered, matched_patterns = _ci_discovery( + discovery, "\n".join(workflow_texts) + ) + if read_errors and not workflow_texts: + ci_outcome = DRIFT + detail = "; ".join(read_errors) + elif discovered: + ci_outcome = PASS + detail = "contract regression test is discovered by executable CI" + else: + ci_outcome = DRIFT + detail = "no configured executable CI discovery rule matched" + if ci_outcome == DRIFT: + drift.append("ci_discovery") + checks.append( + { + "check_id": "ci_discovery", + "outcome": ci_outcome, + "paths": workflow_paths, + "matched_patterns": matched_patterns, + "detail": detail, + } + ) + + if drift: + outcome = DRIFT + reason_code = "LOTUS_CONTRACT_DRIFT" + detail = "non-conforming checks: " + ", ".join(sorted(set(drift))) + else: + outcome = PASS + reason_code = "LOTUS_CONTRACT_CONFORMANT" + detail = "all configured Lotus Family invariants passed" + + unique_files = {(row["path"], row["sha256"]): row for row in files} + return _result( + outcome=outcome, + reason_code=reason_code, + detail=detail, + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=manifest_schema, + checks=checks, + files=list(unique_files.values()), + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Audit one exact repository snapshot against the Lotus Family manifest." + ) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--snapshot-root", type=Path, required=True) + parser.add_argument("--repository-id", required=True) + parser.add_argument("--repository-ref", required=True) + parser.add_argument("--commit-sha", required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args(argv) + + try: + manifest = load_manifest(args.manifest) + result = audit_repository( + manifest, + repository_id=args.repository_id, + snapshot_root=args.snapshot_root, + repository_ref=args.repository_ref, + commit_sha=args.commit_sha, + ) + except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: + result = _manifest_invalid_result( + str(exc), + args.repository_id, + args.repository_ref, + args.commit_sha, + "", + ) + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(result, ensure_ascii=False, sort_keys=True)) + return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(result["outcome"], 3) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py b/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py new file mode 100644 index 00000000..0a2cbd3d --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +import copy +import tempfile +import unittest +from pathlib import Path + +from lotus_family_auditor import DRIFT, PASS, UNKNOWN, audit_repository, load_manifest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +MANIFEST_PATH = ROOT / "manifest" / "lotus-family-v0.1.json" +SHA = "a" * 40 + + +def _write(root: Path, relative_path: str, content: str) -> None: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _discovery_fixture(discovery: dict) -> str: + if discovery.get("strategy") == "pytest_default_discovery": + return ( + "python -m pytest \\\n" + " --junitxml=artifacts/junit.xml \\\n" + " --cov=cml\n" + ) + pattern = discovery["contains_any"][0] + if pattern.endswith(".py"): + return f"python -m pytest {pattern}\n" + return pattern + "\n" + + +def _materialize_repository(snapshot_root: Path, config: dict) -> Path: + repository_root = snapshot_root / config["snapshot_dir"] + terms_by_path: dict[str, list[str]] = {} + for check in config["file_checks"]: + terms_by_path.setdefault(check["path"], []).extend(check["contains_all"]) + for relative_path, terms in terms_by_path.items(): + _write( + repository_root, + relative_path, + "\n".join(dict.fromkeys(terms)) + "\n", + ) + discovery = config["ci_discovery"] + _write( + repository_root, + discovery["workflow_paths"][0], + _discovery_fixture(discovery), + ) + return repository_root + + +class LotusFamilyAuditorTest(unittest.TestCase): + def setUp(self) -> None: + self.manifest = load_manifest(MANIFEST_PATH) + + def config(self, repository_id: str) -> dict: + return next( + row + for row in self.manifest["repositories"] + if row["id"] == repository_id + ) + + def audit(self, repository_id: str, snapshot_root: Path): + return audit_repository( + self.manifest, + repository_id=repository_id, + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + def workflow(self, repository_root: Path) -> Path: + return repository_root / ".github/workflows/ci.yml" + + def discovery(self, result: dict) -> dict: + return next( + row for row in result["checks"] if row["check_id"] == "ci_discovery" + ) + + def assert_ci_drift(self, repository_id: str, command: str) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_repository( + snapshot_root, self.config(repository_id) + ) + self.workflow(repository_root).write_text(command, encoding="utf-8") + result = self.audit(repository_id, snapshot_root) + self.assertEqual(result["outcome"], DRIFT) + self.assertEqual(self.discovery(result)["outcome"], DRIFT) + self.assertEqual(self.discovery(result)["matched_patterns"], []) + + def test_manifest_has_three_repository_adapters_and_audit_only_authority(self): + self.assertEqual(self.manifest["authority"], "audit_only") + self.assertEqual( + {row["id"] for row in self.manifest["repositories"]}, + {"pythia", "cml", "ls"}, + ) + + def test_valid_pythia_snapshot_passes_with_exact_identity_and_hashes(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("pythia")) + result = self.audit("pythia", snapshot_root) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (PASS, "LOTUS_CONTRACT_CONFORMANT"), + ) + self.assertEqual(result["repository"], "safal207/pythiaLabs") + self.assertEqual(result["repository_ref"], "refs/heads/main") + self.assertEqual(result["commit_sha"], SHA) + self.assertTrue(result["files"]) + for row in result["files"]: + self.assertRegex(row["sha256"], r"^[0-9a-f]{64}$") + self.assertFalse(result["authority"]["grants_merge"]) + + def test_valid_cml_default_pytest_discovery_passes(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("cml")) + result = self.audit("cml", snapshot_root) + self.assertEqual(result["outcome"], PASS) + self.assertEqual( + self.discovery(result)["matched_patterns"], + ["python -m pytest"], + ) + + def test_valid_ls_explicit_contract_test_passes(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("ls")) + result = self.audit("ls", snapshot_root) + self.assertEqual(result["outcome"], PASS) + self.assertEqual( + self.discovery(result)["matched_patterns"], + ["tests/test_lotus_docs_contract.py"], + ) + + def test_bilingual_authority_deletion_is_drift(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_repository( + snapshot_root, copy.deepcopy(self.config("cml")) + ) + contract = repository_root / "docs/LOTUS.md" + contract.write_text( + contract.read_text(encoding="utf-8").replace( + "не имеет права собственности", "" + ), + encoding="utf-8", + ) + result = self.audit("cml", snapshot_root) + self.assertEqual(result["outcome"], DRIFT) + check = next( + row + for row in result["checks"] + if row["check_id"] == "bilingual_contract" + ) + self.assertIn("не имеет права собственности", check["missing_terms"]) + + def test_contract_test_not_discovered_by_ci_is_drift(self): + self.assert_ci_drift("ls", "python -m pytest tests/test_other.py\n") + + def test_cml_unrelated_pytest_subset_is_not_full_discovery(self): + self.assert_ci_drift("cml", "python -m pytest tests/test_other.py\n") + + def test_cml_pytest_ignore_of_contract_test_is_drift(self): + self.assert_ci_drift( + "cml", + "python -m pytest --ignore=tests/test_lotus_docs_contract.py\n", + ) + + def test_cml_commented_pytest_command_is_drift(self): + self.assert_ci_drift("cml", "# python -m pytest\n") + + def test_cml_pytest_ignore_directory_covering_contract_test_is_drift(self): + self.assert_ci_drift("cml", "python -m pytest --ignore=tests\n") + + def test_cml_pytest_ignore_glob_covering_contract_test_is_drift(self): + self.assert_ci_drift( + "cml", "python -m pytest --ignore-glob='tests/*'\n" + ) + + def test_cml_pytest_ignore_glob_with_dot_prefix_is_drift(self): + self.assert_ci_drift( + "cml", "python -m pytest --ignore-glob='./tests/*'\n" + ) + + def test_cml_echo_pytest_is_not_executed(self): + self.assert_ci_drift("cml", "echo python -m pytest\n") + + def test_cml_false_and_pytest_is_not_executed(self): + self.assert_ci_drift("cml", "false && python -m pytest\n") + + def test_cml_pytest_k_filter_is_drift(self): + self.assert_ci_drift( + "cml", "python -m pytest -k='not lotus_docs_contract'\n" + ) + + def test_ls_commented_contract_test_is_drift(self): + self.assert_ci_drift( + "ls", "# python -m pytest tests/test_lotus_docs_contract.py\n" + ) + + def test_ls_ignored_contract_test_is_drift(self): + self.assert_ci_drift( + "ls", + "python -m pytest --ignore=tests/test_lotus_docs_contract.py " + "tests/test_lotus_docs_contract.py\n", + ) + + def test_ls_echo_contract_test_is_drift(self): + self.assert_ci_drift( + "ls", "echo python -m pytest tests/test_lotus_docs_contract.py\n" + ) + + def test_pythia_commented_mix_test_is_drift(self): + self.assert_ci_drift("pythia", "# mix test\n") + + def test_pythia_echo_mix_test_is_drift(self): + self.assert_ci_drift("pythia", "echo mix test\n") + + def test_empty_manifest_term_list_is_unknown_not_pass(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("pythia")) + manifest = copy.deepcopy(self.manifest) + manifest["repositories"][0]["file_checks"][0]["contains_all"] = [] + result = audit_repository( + manifest, + repository_id="pythia", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (UNKNOWN, "MANIFEST_INVALID"), + ) + + def test_manifest_path_traversal_is_unknown_and_not_hashed(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("pythia")) + (snapshot_root / "outside.md").write_text( + "all required terms\n", encoding="utf-8" + ) + manifest = copy.deepcopy(self.manifest) + manifest["repositories"][0]["file_checks"][0]["path"] = ( + "../outside.md" + ) + result = audit_repository( + manifest, + repository_id="pythia", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (UNKNOWN, "MANIFEST_INVALID"), + ) + self.assertEqual(result["files"], []) + + def test_missing_repository_snapshot_is_unknown_not_pass(self): + with tempfile.TemporaryDirectory() as directory: + result = self.audit("pythia", Path(directory)) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (UNKNOWN, "SNAPSHOT_UNAVAILABLE"), + ) + + def test_invalid_commit_sha_is_unknown_not_pass(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + _materialize_repository(snapshot_root, self.config("pythia")) + result = audit_repository( + self.manifest, + repository_id="pythia", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha="main", + ) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (UNKNOWN, "COMMIT_SHA_INVALID"), + ) + + def test_file_hash_changes_when_checked_content_changes(self): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_repository( + snapshot_root, self.config("pythia") + ) + first = self.audit("pythia", snapshot_root) + contract = repository_root / "LOTUS.md" + contract.write_text( + contract.read_text(encoding="utf-8") + "extra evidence\n", + encoding="utf-8", + ) + second = self.audit("pythia", snapshot_root) + first_hash = next( + row["sha256"] + for row in first["files"] + if row["path"] == "LOTUS.md" + ) + second_hash = next( + row["sha256"] + for row in second["files"] + if row["path"] == "LOTUS.md" + ) + self.assertNotEqual(first_hash, second_hash) + + +if __name__ == "__main__": + unittest.main() diff --git a/standards/lotus-family/conformance/test_lotus_family_auditor.py b/standards/lotus-family/conformance/test_lotus_family_auditor.py index 0a2cbd3d..03919fc9 100644 --- a/standards/lotus-family/conformance/test_lotus_family_auditor.py +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -1,318 +1,87 @@ -from __future__ import annotations - -import copy import tempfile import unittest from pathlib import Path - -from lotus_family_auditor import DRIFT, PASS, UNKNOWN, audit_repository, load_manifest - -HERE = Path(__file__).resolve().parent -ROOT = HERE.parent -MANIFEST_PATH = ROOT / "manifest" / "lotus-family-v0.1.json" -SHA = "a" * 40 +import lotus_family_auditor_legacy_tests as old -def _write(root: Path, relative_path: str, content: str) -> None: - path = root / relative_path - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") +def workflow(command, env=""): + env_block = "env:\n" + "\n".join(f" {x}" for x in env.splitlines()) + "\n" if env else "" + body = "\n".join(f" {x}" if x else "" for x in command.splitlines()) + return ("name: CI\n" + env_block + "jobs:\n test:\n runs-on: ubuntu-latest\n" + " steps:\n - name: Run tests\n run: |\n" + body + "\n") -def _discovery_fixture(discovery: dict) -> str: +def fixture(discovery): if discovery.get("strategy") == "pytest_default_discovery": - return ( - "python -m pytest \\\n" - " --junitxml=artifacts/junit.xml \\\n" - " --cov=cml\n" - ) - pattern = discovery["contains_any"][0] - if pattern.endswith(".py"): - return f"python -m pytest {pattern}\n" - return pattern + "\n" - - -def _materialize_repository(snapshot_root: Path, config: dict) -> Path: - repository_root = snapshot_root / config["snapshot_dir"] - terms_by_path: dict[str, list[str]] = {} - for check in config["file_checks"]: - terms_by_path.setdefault(check["path"], []).extend(check["contains_all"]) - for relative_path, terms in terms_by_path.items(): - _write( - repository_root, - relative_path, - "\n".join(dict.fromkeys(terms)) + "\n", - ) - discovery = config["ci_discovery"] - _write( - repository_root, - discovery["workflow_paths"][0], - _discovery_fixture(discovery), - ) - return repository_root - - -class LotusFamilyAuditorTest(unittest.TestCase): - def setUp(self) -> None: - self.manifest = load_manifest(MANIFEST_PATH) - - def config(self, repository_id: str) -> dict: - return next( - row - for row in self.manifest["repositories"] - if row["id"] == repository_id - ) - - def audit(self, repository_id: str, snapshot_root: Path): - return audit_repository( - self.manifest, - repository_id=repository_id, - snapshot_root=snapshot_root, - repository_ref="refs/heads/main", - commit_sha=SHA, - ) - - def workflow(self, repository_root: Path) -> Path: - return repository_root / ".github/workflows/ci.yml" - - def discovery(self, result: dict) -> dict: - return next( - row for row in result["checks"] if row["check_id"] == "ci_discovery" - ) - - def assert_ci_drift(self, repository_id: str, command: str) -> None: - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - repository_root = _materialize_repository( - snapshot_root, self.config(repository_id) - ) - self.workflow(repository_root).write_text(command, encoding="utf-8") - result = self.audit(repository_id, snapshot_root) - self.assertEqual(result["outcome"], DRIFT) - self.assertEqual(self.discovery(result)["outcome"], DRIFT) - self.assertEqual(self.discovery(result)["matched_patterns"], []) - - def test_manifest_has_three_repository_adapters_and_audit_only_authority(self): - self.assertEqual(self.manifest["authority"], "audit_only") - self.assertEqual( - {row["id"] for row in self.manifest["repositories"]}, - {"pythia", "cml", "ls"}, - ) - - def test_valid_pythia_snapshot_passes_with_exact_identity_and_hashes(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - _materialize_repository(snapshot_root, self.config("pythia")) - result = self.audit("pythia", snapshot_root) - self.assertEqual( - (result["outcome"], result["reason_code"]), - (PASS, "LOTUS_CONTRACT_CONFORMANT"), - ) - self.assertEqual(result["repository"], "safal207/pythiaLabs") - self.assertEqual(result["repository_ref"], "refs/heads/main") - self.assertEqual(result["commit_sha"], SHA) - self.assertTrue(result["files"]) - for row in result["files"]: - self.assertRegex(row["sha256"], r"^[0-9a-f]{64}$") - self.assertFalse(result["authority"]["grants_merge"]) - - def test_valid_cml_default_pytest_discovery_passes(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - _materialize_repository(snapshot_root, self.config("cml")) - result = self.audit("cml", snapshot_root) - self.assertEqual(result["outcome"], PASS) - self.assertEqual( - self.discovery(result)["matched_patterns"], - ["python -m pytest"], - ) - - def test_valid_ls_explicit_contract_test_passes(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - _materialize_repository(snapshot_root, self.config("ls")) - result = self.audit("ls", snapshot_root) - self.assertEqual(result["outcome"], PASS) - self.assertEqual( - self.discovery(result)["matched_patterns"], - ["tests/test_lotus_docs_contract.py"], - ) - - def test_bilingual_authority_deletion_is_drift(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - repository_root = _materialize_repository( - snapshot_root, copy.deepcopy(self.config("cml")) - ) - contract = repository_root / "docs/LOTUS.md" - contract.write_text( - contract.read_text(encoding="utf-8").replace( - "не имеет права собственности", "" - ), - encoding="utf-8", - ) - result = self.audit("cml", snapshot_root) - self.assertEqual(result["outcome"], DRIFT) - check = next( - row - for row in result["checks"] - if row["check_id"] == "bilingual_contract" - ) - self.assertIn("не имеет права собственности", check["missing_terms"]) + command = "python -m pytest \\\n --junitxml=artifacts/junit.xml \\\n --cov=cml\n" + else: + pattern = discovery["contains_any"][0] + command = f"python -m pytest {pattern}\n" if pattern.endswith(".py") else pattern + "\n" + return workflow(command) - def test_contract_test_not_discovered_by_ci_is_drift(self): - self.assert_ci_drift("ls", "python -m pytest tests/test_other.py\n") - def test_cml_unrelated_pytest_subset_is_not_full_discovery(self): - self.assert_ci_drift("cml", "python -m pytest tests/test_other.py\n") +def assert_drift(self, repo_id, command, *, raw=False, env=""): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + repo = old._materialize_repository(root, self.config(repo_id)) + self.workflow(repo).write_text(command if raw else workflow(command, env), encoding="utf-8") + result = self.audit(repo_id, root) + self.assertEqual(result["outcome"], old.DRIFT) + self.assertEqual(self.discovery(result)["outcome"], old.DRIFT) + self.assertEqual(self.discovery(result)["matched_patterns"], []) - def test_cml_pytest_ignore_of_contract_test_is_drift(self): - self.assert_ci_drift( - "cml", - "python -m pytest --ignore=tests/test_lotus_docs_contract.py\n", - ) - def test_cml_commented_pytest_command_is_drift(self): - self.assert_ci_drift("cml", "# python -m pytest\n") +old._discovery_fixture = fixture +old.LotusFamilyAuditorTest.assert_ci_drift = assert_drift - def test_cml_pytest_ignore_directory_covering_contract_test_is_drift(self): - self.assert_ci_drift("cml", "python -m pytest --ignore=tests\n") - def test_cml_pytest_ignore_glob_covering_contract_test_is_drift(self): - self.assert_ci_drift( - "cml", "python -m pytest --ignore-glob='tests/*'\n" - ) +def bare(self): + self.assert_ci_drift("cml", "python -m pytest\n", raw=True) - def test_cml_pytest_ignore_glob_with_dot_prefix_is_drift(self): - self.assert_ci_drift( - "cml", "python -m pytest --ignore-glob='./tests/*'\n" - ) - def test_cml_echo_pytest_is_not_executed(self): - self.assert_ci_drift("cml", "echo python -m pytest\n") +def workflow_env_text(self): + self.assert_ci_drift("cml", "name: CI\nenv:\n NOTE: |\n python -m pytest\n" + "jobs:\n test:\n runs-on: ubuntu-latest\n steps:\n" + " - uses: actions/checkout@v4\n", raw=True) - def test_cml_false_and_pytest_is_not_executed(self): - self.assert_ci_drift("cml", "false && python -m pytest\n") - def test_cml_pytest_k_filter_is_drift(self): - self.assert_ci_drift( - "cml", "python -m pytest -k='not lotus_docs_contract'\n" - ) +def step_env_text(self): + self.assert_ci_drift("cml", "name: CI\njobs:\n test:\n runs-on: ubuntu-latest\n" + " steps:\n - name: Metadata only\n env:\n" + " NOTE: python -m pytest\n uses: actions/checkout@v4\n", raw=True) - def test_ls_commented_contract_test_is_drift(self): - self.assert_ci_drift( - "ls", "# python -m pytest tests/test_lotus_docs_contract.py\n" - ) - def test_ls_ignored_contract_test_is_drift(self): - self.assert_ci_drift( - "ls", - "python -m pytest --ignore=tests/test_lotus_docs_contract.py " - "tests/test_lotus_docs_contract.py\n", - ) +def cml_addopts(self): + self.assert_ci_drift("cml", "PYTEST_ADDOPTS='--ignore=tests/test_lotus_docs_contract.py' python -m pytest\n") - def test_ls_echo_contract_test_is_drift(self): - self.assert_ci_drift( - "ls", "echo python -m pytest tests/test_lotus_docs_contract.py\n" - ) - def test_pythia_commented_mix_test_is_drift(self): - self.assert_ci_drift("pythia", "# mix test\n") +def ls_addopts(self): + self.assert_ci_drift("ls", "PYTEST_ADDOPTS='--ignore=tests/test_lotus_docs_contract.py' " + "python -m pytest tests/test_lotus_docs_contract.py\n") - def test_pythia_echo_mix_test_is_drift(self): - self.assert_ci_drift("pythia", "echo mix test\n") - def test_empty_manifest_term_list_is_unknown_not_pass(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - _materialize_repository(snapshot_root, self.config("pythia")) - manifest = copy.deepcopy(self.manifest) - manifest["repositories"][0]["file_checks"][0]["contains_all"] = [] - result = audit_repository( - manifest, - repository_id="pythia", - snapshot_root=snapshot_root, - repository_ref="refs/heads/main", - commit_sha=SHA, - ) - self.assertEqual( - (result["outcome"], result["reason_code"]), - (UNKNOWN, "MANIFEST_INVALID"), - ) +def yaml_addopts(self): + self.assert_ci_drift("cml", "python -m pytest\n", + env="PYTEST_ADDOPTS: --ignore=tests/test_lotus_docs_contract.py") - def test_manifest_path_traversal_is_unknown_and_not_hashed(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - _materialize_repository(snapshot_root, self.config("pythia")) - (snapshot_root / "outside.md").write_text( - "all required terms\n", encoding="utf-8" - ) - manifest = copy.deepcopy(self.manifest) - manifest["repositories"][0]["file_checks"][0]["path"] = ( - "../outside.md" - ) - result = audit_repository( - manifest, - repository_id="pythia", - snapshot_root=snapshot_root, - repository_ref="refs/heads/main", - commit_sha=SHA, - ) - self.assertEqual( - (result["outcome"], result["reason_code"]), - (UNKNOWN, "MANIFEST_INVALID"), - ) - self.assertEqual(result["files"], []) - def test_missing_repository_snapshot_is_unknown_not_pass(self): - with tempfile.TemporaryDirectory() as directory: - result = self.audit("pythia", Path(directory)) - self.assertEqual( - (result["outcome"], result["reason_code"]), - (UNKNOWN, "SNAPSHOT_UNAVAILABLE"), - ) +def pytest_plugins(self): + self.assert_ci_drift("cml", "PYTEST_PLUGINS=custom_plugin python -m pytest\n") - def test_invalid_commit_sha_is_unknown_not_pass(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - _materialize_repository(snapshot_root, self.config("pythia")) - result = audit_repository( - self.manifest, - repository_id="pythia", - snapshot_root=snapshot_root, - repository_ref="refs/heads/main", - commit_sha="main", - ) - self.assertEqual( - (result["outcome"], result["reason_code"]), - (UNKNOWN, "COMMIT_SHA_INVALID"), - ) - def test_file_hash_changes_when_checked_content_changes(self): - with tempfile.TemporaryDirectory() as directory: - snapshot_root = Path(directory) - repository_root = _materialize_repository( - snapshot_root, self.config("pythia") - ) - first = self.audit("pythia", snapshot_root) - contract = repository_root / "LOTUS.md" - contract.write_text( - contract.read_text(encoding="utf-8") + "extra evidence\n", - encoding="utf-8", - ) - second = self.audit("pythia", snapshot_root) - first_hash = next( - row["sha256"] - for row in first["files"] - if row["path"] == "LOTUS.md" - ) - second_hash = next( - row["sha256"] - for row in second["files"] - if row["path"] == "LOTUS.md" - ) - self.assertNotEqual(first_hash, second_hash) +new_tests = { + "test_bare_shell_text_is_not_a_run_step": bare, + "test_pytest_text_in_workflow_env_value_is_not_executed": workflow_env_text, + "test_pytest_text_in_step_env_value_is_not_executed": step_env_text, + "test_cml_pytest_addopts_assignment_is_drift": cml_addopts, + "test_ls_pytest_addopts_assignment_is_drift": ls_addopts, + "test_workflow_pytest_addopts_env_is_drift": yaml_addopts, + "test_pytest_plugin_environment_assignment_is_drift": pytest_plugins, +} +for name, method in new_tests.items(): + setattr(old.LotusFamilyAuditorTest, name, method) +LotusFamilyAuditorTest = old.LotusFamilyAuditorTest if __name__ == "__main__": unittest.main() From c990749c2d102b37bdc7cba466e6746a94937585 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:14:11 +0300 Subject: [PATCH 25/79] test: reject fake steps outside GitHub jobs --- .../conformance/lotus_family_auditor.py | 34 ++++++++++++++++--- .../conformance/test_lotus_family_auditor.py | 6 ++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_auditor.py b/standards/lotus-family/conformance/lotus_family_auditor.py index 540a2683..32cce56d 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor.py +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -154,6 +154,33 @@ def _scalar_ranges(lines: list[str]) -> dict[int, tuple[int, str]]: return ranges + +def _github_actions_steps_indices( + lines: list[str], scalar_body_lines: set[int] +) -> set[int]: + stack: list[tuple[int, str]] = [] + valid: set[int] = set() + for index, line in enumerate(lines): + if index in scalar_body_lines: + continue + header = _yaml_header(line) + if header is None or header[1]: + continue + _, _, key_indent, key, value = header + while stack and stack[-1][0] >= key_indent: + stack.pop() + parent_keys = [entry[1] for entry in stack] + if ( + key == "steps" + and _inline_yaml_scalar(value) is None + and len(parent_keys) == 2 + and parent_keys[0] == "jobs" + ): + valid.add(index) + if _inline_yaml_scalar(value) is None and _scalar_indicator(value) is None: + stack.append((key_indent, key)) + return valid + def _github_actions_run_scripts(text: str) -> list[str]: lines = text.splitlines() scalar_ranges = _scalar_ranges(lines) @@ -162,6 +189,7 @@ def _github_actions_run_scripts(text: str) -> list[str]: for start, (end, _) in scalar_ranges.items() for line_index in range(start + 1, end) } + valid_steps = _github_actions_steps_indices(lines, scalar_body_lines) scripts: list[str] = [] index = 0 @@ -170,11 +198,7 @@ def _github_actions_run_scripts(text: str) -> list[str]: index += 1 continue header = _yaml_header(lines[index]) - if ( - header is None - or header[3] != "steps" - or _inline_yaml_scalar(header[4]) is not None - ): + if index not in valid_steps or header is None: index += 1 continue diff --git a/standards/lotus-family/conformance/test_lotus_family_auditor.py b/standards/lotus-family/conformance/test_lotus_family_auditor.py index 03919fc9..b32277e5 100644 --- a/standards/lotus-family/conformance/test_lotus_family_auditor.py +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -51,6 +51,11 @@ def step_env_text(self): " NOTE: python -m pytest\n uses: actions/checkout@v4\n", raw=True) +def fake_steps_in_metadata(self): + self.assert_ci_drift("cml", "name: CI\nmetadata:\n fake-job:\n steps:\n" + " - run: python -m pytest\njobs: {}\n", raw=True) + + def cml_addopts(self): self.assert_ci_drift("cml", "PYTEST_ADDOPTS='--ignore=tests/test_lotus_docs_contract.py' python -m pytest\n") @@ -73,6 +78,7 @@ def pytest_plugins(self): "test_bare_shell_text_is_not_a_run_step": bare, "test_pytest_text_in_workflow_env_value_is_not_executed": workflow_env_text, "test_pytest_text_in_step_env_value_is_not_executed": step_env_text, + "test_steps_outside_jobs_are_not_executable": fake_steps_in_metadata, "test_cml_pytest_addopts_assignment_is_drift": cml_addopts, "test_ls_pytest_addopts_assignment_is_drift": ls_addopts, "test_workflow_pytest_addopts_env_is_drift": yaml_addopts, From 9aa8467f43468b642e4c1f14dab477a1b5e89d19 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:44:01 +0300 Subject: [PATCH 26/79] feat: add causal test graph and fail-closed routes --- standards/lotus-family/DECISION-CODES.md | 10 +- standards/lotus-family/README.md | 44 +- standards/lotus-family/ROADMAP.md | 15 +- .../lotus-family/causality/TRACEABILITY.md | 37 + .../lotus-family-causality-v0.1.json | 76 ++ .../causality/test-paths-v0.1.json | 125 +++ .../conformance/lotus_family_auditor.py | 383 +--------- .../conformance/lotus_family_auditor_core.py | 709 +----------------- .../conformance/lotus_family_runtime.py | 141 ++++ .../conformance/lotus_family_schema.py | 182 +++++ .../conformance/lotus_family_workflow.py | 427 +++++++++++ .../conformance/test_causality_model.py | 194 +++++ .../manifest/lotus-family-v0.1.json | 7 +- 13 files changed, 1241 insertions(+), 1109 deletions(-) create mode 100644 standards/lotus-family/causality/TRACEABILITY.md create mode 100644 standards/lotus-family/causality/lotus-family-causality-v0.1.json create mode 100644 standards/lotus-family/causality/test-paths-v0.1.json create mode 100644 standards/lotus-family/conformance/lotus_family_runtime.py create mode 100644 standards/lotus-family/conformance/lotus_family_schema.py create mode 100644 standards/lotus-family/conformance/lotus_family_workflow.py create mode 100644 standards/lotus-family/conformance/test_causality_model.py diff --git a/standards/lotus-family/DECISION-CODES.md b/standards/lotus-family/DECISION-CODES.md index 68e9f940..50319cec 100644 --- a/standards/lotus-family/DECISION-CODES.md +++ b/standards/lotus-family/DECISION-CODES.md @@ -2,12 +2,14 @@ | Outcome | Reason code | Meaning | |---|---|---| -| `PASS` | `LOTUS_CONTRACT_CONFORMANT` | Every configured invariant passed for the exact snapshot. | -| `DRIFT` | `LOTUS_CONTRACT_DRIFT` | A required contract, test, or CI discovery invariant is missing. | +| `PASS` | `LOTUS_CONTRACT_CONFORMANT` | Every configured invariant passed for the supplied snapshot and caller-provided identity claims. | +| `DRIFT` | `LOTUS_CONTRACT_DRIFT` | A required contract, test, or executable CI discovery invariant is missing. | | `UNKNOWN` | `SNAPSHOT_UNAVAILABLE` | The repository snapshot is unavailable. | -| `UNKNOWN` | `COMMIT_SHA_INVALID` | The supplied commit identity is not an exact 40-character SHA. | -| `UNKNOWN` | `REPOSITORY_REF_MISSING` | The exact repository ref was not supplied. | +| `UNKNOWN` | `COMMIT_SHA_INVALID` | The supplied commit claim is not a lowercase 40-character SHA. | +| `UNKNOWN` | `REPOSITORY_REF_MISSING` | The repository ref claim was not supplied. | | `UNKNOWN` | `REPOSITORY_NOT_CONFIGURED` | No manifest adapter exists for the repository ID. | | `UNKNOWN` | `MANIFEST_INVALID` | The manifest cannot be parsed or violates the audit-only boundary. | Consumers must branch on `outcome` and `reason_code`, not on human-readable detail. +A `PASS` does not verify remote provenance and grants no ownership, approval, +execution, delivery, deployment, or merge authority. diff --git a/standards/lotus-family/README.md b/standards/lotus-family/README.md index 672f567c..d375de4c 100644 --- a/standards/lotus-family/README.md +++ b/standards/lotus-family/README.md @@ -5,12 +5,38 @@ across Pythia, CML, and LS. ## Outcomes -- `PASS` — every configured invariant was verified for the supplied repository - snapshot and exact commit SHA. +- `PASS` — every configured content and executable-CI invariant passed for the + supplied snapshot and the caller-provided repository, ref, and commit claims. - `DRIFT` — the snapshot is available, but a required contract term, regression test, or CI discovery rule is missing. -- `UNKNOWN` — the repository snapshot, exact ref, commit SHA, or manifest could - not be verified. `UNKNOWN` is never promoted to `PASS`. +- `UNKNOWN` — the repository snapshot, ref claim, commit claim, or manifest could + not be evaluated safely. `UNKNOWN` is never promoted to `PASS`. + +## Identity assurance + +Version 0.1 does not fetch a remote repository and does not prove commit +reachability or a clean working tree. Repository, ref, and commit values are +caller-provided claims recorded in evidence. Results expose +`identity_assurance.mode = caller_claim_only` so consumers cannot mistake these +claims for verified provenance. + +Remote identity verification, trusted materialization, and evidence attestation +remain follow-up work. Until then, `PASS` means conformance of the supplied +snapshot contents, not cryptographic proof that the snapshot came from the +claimed remote commit. + +## Causal testing model + +The primary test model lives in: + +- `causality/lotus-family-causality-v0.1.json` — causes, controls, risks, evidence, + outcomes, and authority boundaries; +- `causality/test-paths-v0.1.json` — executable end-to-end routes through that graph; +- `causality/TRACEABILITY.md` — a reviewable matrix derived from the routes. + +Every new blocker must add or reuse a causal node and include an executable route. +This prevents isolated regression tests from hiding missing relationships between +workflow structure, command execution, test selection, evidence, and verdicts. ## Boundary @@ -18,8 +44,7 @@ The auditor is read-only and `audit_only`. Its result does not grant ownership, approval, execution, delivery, deployment, or merge authority. It does not fetch repositories, call GitHub, merge pull requests, or deploy -software. An integration must materialize a repository snapshot at the exact -commit it claims to audit. +software. An integration materializes the repository snapshot it wants audited. ## Snapshot layout @@ -38,7 +63,7 @@ Each directory must contain the files named by ```bash python standards/lotus-family/conformance/lotus_family_auditor.py \ --manifest standards/lotus-family/manifest/lotus-family-v0.1.json \ - --snapshot-root /path/to/exact-snapshots \ + --snapshot-root /path/to/snapshots \ --repository-id pythia \ --repository-ref refs/heads/main \ --commit-sha 0123456789abcdef0123456789abcdef01234567 \ @@ -47,5 +72,6 @@ python standards/lotus-family/conformance/lotus_family_auditor.py \ Exit codes are `0` for `PASS`, `2` for `DRIFT`, and `3` for `UNKNOWN`. -The evidence artifact records the exact repository identity, ref, commit SHA, -check outcomes, checked file paths, and SHA-256 hashes. +The evidence artifact records the caller-provided repository identity claims, +check outcomes, checked file paths, SHA-256 hashes, identity-assurance limits, +and the audit-only authority boundary. diff --git a/standards/lotus-family/ROADMAP.md b/standards/lotus-family/ROADMAP.md index af2f7268..c32d14de 100644 --- a/standards/lotus-family/ROADMAP.md +++ b/standards/lotus-family/ROADMAP.md @@ -3,15 +3,18 @@ ## v0.1 - manifest for Pythia, CML, and LS; -- exact repository ref and commit SHA requirement; +- syntactically exact repository ref and commit SHA claims; +- explicit `caller_claim_only` identity assurance; - `PASS / DRIFT / UNKNOWN` outcomes; -- bilingual contract, authority firewall, regression test, and CI discovery checks; -- SHA-256 evidence artifact; +- bilingual contract, authority firewall, regression test, and executable CI discovery checks; +- causal graph, executable causal routes, and derived traceability; +- SHA-256 evidence from the same bytes used for evaluation; - audit-only authority boundary. ## Follow-up -- materialize exact snapshots from a trusted integration; -- verify remote repository identity and commit reachability; +- materialize snapshots through a trusted integration; +- verify remote repository identity, commit reachability, and clean snapshot state; +- sign or attest evidence artifacts without granting execution authority; - aggregate three repository results into one family report; -- sign or attest evidence artifacts without granting execution authority. +- generate selected regression fixtures from causal routes while keeping human review. diff --git a/standards/lotus-family/causality/TRACEABILITY.md b/standards/lotus-family/causality/TRACEABILITY.md new file mode 100644 index 00000000..6c3a5223 --- /dev/null +++ b/standards/lotus-family/causality/TRACEABILITY.md @@ -0,0 +1,37 @@ +# Lotus Family causal traceability + +The causality graph is the primary behavioral model. This table is the human-readable coverage ledger. A route is valid only when each adjacent node pair is an edge in `lotus-family-causality-v0.1.json`, and the route executes to the declared outcome in `test_causality_model.py`. + +## Policy + +- New blockers add or reuse a causal node before a regression route is accepted. +- `PASS` routes must reach evidence produced from the same bytes that were evaluated. +- `DRIFT` routes model a broken invariant or a false-positive CI path. +- `UNKNOWN` routes model evidence that cannot be evaluated safely. +- All outcomes remain advisory and grant no merge or execution authority. + +## Routes + +| Route | Repository | Scenario | Expected | Causal path | +|---|---|---|---|---| +| `CML-PASS-001` | `cml` | `valid` | `PASS` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_runnable → control.step_enabled → control.direct_run → control.test_env_clean → control.shell_straight_line → control.contract_test_selected → evidence.same_bytes_hashed → outcome.pass → authority.advisory_only | +| `LS-PASS-001` | `ls` | `valid` | `PASS` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_runnable → control.step_enabled → control.direct_run → control.test_env_clean → control.shell_straight_line → control.contract_test_selected → evidence.same_bytes_hashed → outcome.pass → authority.advisory_only | +| `PYTHIA-PASS-001` | `pythia` | `valid` | `PASS` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_runnable → control.step_enabled → control.direct_run → control.test_env_clean → control.shell_straight_line → control.contract_test_selected → evidence.same_bytes_hashed → outcome.pass → authority.advisory_only | +| `CI-NONRUN-001` | `cml` | `workflow` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_runnable → control.step_enabled → control.non_run_text → risk.false_ci_pass → outcome.drift | +| `CI-STEP-SKIP-001` | `cml` | `workflow` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_runnable → control.step_skipped → risk.false_ci_pass → outcome.drift | +| `CI-JOB-SKIP-001` | `cml` | `workflow` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_not_runnable → risk.false_ci_pass → outcome.drift | +| `CI-NO-RUNNER-001` | `cml` | `workflow` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_not_runnable → risk.false_ci_pass → outcome.drift | +| `CI-QUOTED-ENV-001` | `cml` | `workflow` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_runnable → control.step_enabled → control.direct_run → control.test_env_override → risk.false_ci_pass → outcome.drift | +| `CI-SHELL-CONTROL-001` | `cml` | `workflow` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_runnable → control.step_enabled → control.direct_run → control.test_env_clean → control.shell_control_flow → risk.false_ci_pass → outcome.drift | +| `CML-SUBSET-001` | `cml` | `workflow` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_runnable → control.step_enabled → control.direct_run → control.test_env_clean → control.shell_straight_line → control.contract_test_excluded → risk.false_ci_pass → outcome.drift | +| `PYTHIA-SUBSET-001` | `pythia` | `workflow` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_runnable → control.step_enabled → control.direct_run → control.test_env_clean → control.shell_straight_line → control.contract_test_excluded → risk.false_ci_pass → outcome.drift | +| `CI-FAKE-STEPS-001` | `cml` | `workflow` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_present → control.workflow_job_not_runnable → risk.false_ci_pass → outcome.drift | +| `IDENTITY-INVALID-001` | `cml` | `invalid_commit` | `UNKNOWN` | input.snapshot_present → input.identity_claims_invalid → outcome.unknown | +| `SNAPSHOT-MISSING-001` | `cml` | `missing_snapshot` | `UNKNOWN` | input.snapshot_missing → outcome.unknown | +| `CONTRACT-DRIFT-001` | `cml` | `missing_term` | `DRIFT` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → control.manifest_valid → control.contract_terms_missing → outcome.drift | +| `IDENTITY-LIMIT-001` | `cml` | `valid` | `PASS` | input.snapshot_present → input.identity_claims_well_formed → limitation.identity_unverified → risk.identity_overclaim → authority.advisory_only | +| `MANIFEST-INVALID-001` | `cml` | `invalid_manifest` | `UNKNOWN` | control.manifest_invalid → outcome.unknown | + +## Coverage interpretation + +Node coverage shows that a condition or decision is represented. Edge coverage shows that a causal relationship is exercised. Route coverage shows that an end-to-end behavioral path reaches the expected `PASS`, `DRIFT`, or `UNKNOWN` result. The matrix remains useful for review and audit, but it is derived from the causal routes rather than acting as the source of truth. diff --git a/standards/lotus-family/causality/lotus-family-causality-v0.1.json b/standards/lotus-family/causality/lotus-family-causality-v0.1.json new file mode 100644 index 00000000..39277d21 --- /dev/null +++ b/standards/lotus-family/causality/lotus-family-causality-v0.1.json @@ -0,0 +1,76 @@ +{ + "schema_version": "pythia.lotus_causality_graph.v0.1", + "authority": "audit_only", + "scope": "Lotus Family contract and executable CI discovery", + "nodes": [ + {"id": "input.snapshot_present", "type": "precondition", "description": "Materialized repository snapshot exists"}, + {"id": "input.snapshot_missing", "type": "precondition", "description": "Materialized repository snapshot is unavailable"}, + {"id": "input.identity_claims_well_formed", "type": "claim", "description": "Repository ref and commit SHA claims are syntactically valid"}, + {"id": "input.identity_claims_invalid", "type": "claim", "description": "Repository ref or commit SHA claim is invalid"}, + {"id": "limitation.identity_unverified", "type": "limitation", "description": "Remote repository identity and commit reachability are not verified locally"}, + {"id": "control.manifest_valid", "type": "control", "description": "Manifest schema and paths are valid"}, + {"id": "control.manifest_invalid", "type": "control", "description": "Manifest schema or paths are invalid"}, + {"id": "control.contract_terms_present", "type": "control", "description": "Required contract terms are present"}, + {"id": "control.contract_terms_missing", "type": "control", "description": "Required contract terms are missing"}, + {"id": "control.workflow_job_runnable", "type": "control", "description": "Workflow contains a jobs. with runs-on and no uncertain or false job condition"}, + {"id": "control.workflow_job_not_runnable", "type": "control", "description": "No provably runnable job contains the candidate test step"}, + {"id": "control.step_enabled", "type": "control", "description": "Candidate step has no condition or an explicitly true condition"}, + {"id": "control.step_skipped", "type": "control", "description": "Candidate step is disabled or conditionally uncertain"}, + {"id": "control.direct_run", "type": "control", "description": "Candidate command is in a direct run field"}, + {"id": "control.non_run_text", "type": "control", "description": "Candidate command appears only in metadata or a non-executable mapping"}, + {"id": "control.test_env_clean", "type": "control", "description": "No PYTEST_* environment assignment can alter collection"}, + {"id": "control.test_env_override", "type": "control", "description": "A PYTEST_* environment assignment can alter collection"}, + {"id": "control.shell_straight_line", "type": "control", "description": "Run script is straight-line shell without control-flow constructs"}, + {"id": "control.shell_control_flow", "type": "control", "description": "Run script contains shell control flow or unreachable branches"}, + {"id": "control.contract_test_selected", "type": "control", "description": "Command proves full default discovery or selects the configured Lotus test"}, + {"id": "control.contract_test_excluded", "type": "control", "description": "Command selects an unrelated subset or can exclude the Lotus test"}, + {"id": "evidence.same_bytes_hashed", "type": "evidence", "description": "SHA-256 is computed from the same bytes decoded and evaluated"}, + {"id": "risk.false_ci_pass", "type": "risk", "description": "Non-executed or filtered test text could be mistaken for CI coverage"}, + {"id": "risk.identity_overclaim", "type": "risk", "description": "Caller-provided identity claims could be mistaken for verified remote provenance"}, + {"id": "authority.advisory_only", "type": "boundary", "description": "Audit evidence grants no execution, approval, delivery, or merge authority"}, + {"id": "outcome.pass", "type": "outcome", "description": "Configured content and CI-discovery invariants passed for the supplied snapshot claims"}, + {"id": "outcome.drift", "type": "outcome", "description": "A required contract or CI-discovery invariant is missing"}, + {"id": "outcome.unknown", "type": "outcome", "description": "The snapshot, identity claim, or manifest cannot be evaluated safely"} + ], + "edges": [ + {"id": "E01", "source": "input.snapshot_present", "target": "input.identity_claims_well_formed", "relation": "enables"}, + {"id": "E02", "source": "input.snapshot_present", "target": "input.identity_claims_invalid", "relation": "may_reveal"}, + {"id": "E03", "source": "input.snapshot_missing", "target": "outcome.unknown", "relation": "causes"}, + {"id": "E04", "source": "input.identity_claims_invalid", "target": "outcome.unknown", "relation": "causes"}, + {"id": "E05", "source": "input.identity_claims_well_formed", "target": "limitation.identity_unverified", "relation": "retains_limitation"}, + {"id": "E06", "source": "limitation.identity_unverified", "target": "control.manifest_valid", "relation": "permits_bounded_content_audit"}, + {"id": "E07", "source": "limitation.identity_unverified", "target": "risk.identity_overclaim", "relation": "creates_if_misrepresented"}, + {"id": "E08", "source": "risk.identity_overclaim", "target": "authority.advisory_only", "relation": "is_bounded_by"}, + {"id": "E09", "source": "control.manifest_invalid", "target": "outcome.unknown", "relation": "causes"}, + {"id": "E10", "source": "control.manifest_valid", "target": "control.contract_terms_present", "relation": "enables_check"}, + {"id": "E11", "source": "control.manifest_valid", "target": "control.contract_terms_missing", "relation": "may_reveal"}, + {"id": "E12", "source": "control.contract_terms_missing", "target": "outcome.drift", "relation": "causes"}, + {"id": "E13", "source": "control.contract_terms_present", "target": "control.workflow_job_runnable", "relation": "enables_ci_analysis"}, + {"id": "E14", "source": "control.contract_terms_present", "target": "control.workflow_job_not_runnable", "relation": "may_reveal"}, + {"id": "E15", "source": "control.workflow_job_not_runnable", "target": "risk.false_ci_pass", "relation": "would_create_without_guard"}, + {"id": "E16", "source": "control.workflow_job_runnable", "target": "control.step_enabled", "relation": "enables"}, + {"id": "E17", "source": "control.workflow_job_runnable", "target": "control.step_skipped", "relation": "may_reveal"}, + {"id": "E18", "source": "control.step_skipped", "target": "risk.false_ci_pass", "relation": "would_create_without_guard"}, + {"id": "E19", "source": "control.step_enabled", "target": "control.direct_run", "relation": "enables"}, + {"id": "E20", "source": "control.step_enabled", "target": "control.non_run_text", "relation": "may_reveal"}, + {"id": "E21", "source": "control.non_run_text", "target": "risk.false_ci_pass", "relation": "would_create_without_guard"}, + {"id": "E22", "source": "control.direct_run", "target": "control.test_env_clean", "relation": "requires"}, + {"id": "E23", "source": "control.direct_run", "target": "control.test_env_override", "relation": "may_reveal"}, + {"id": "E24", "source": "control.test_env_override", "target": "risk.false_ci_pass", "relation": "would_create_without_guard"}, + {"id": "E25", "source": "control.test_env_clean", "target": "control.shell_straight_line", "relation": "requires"}, + {"id": "E26", "source": "control.test_env_clean", "target": "control.shell_control_flow", "relation": "may_reveal"}, + {"id": "E27", "source": "control.shell_control_flow", "target": "risk.false_ci_pass", "relation": "would_create_without_guard"}, + {"id": "E28", "source": "control.shell_straight_line", "target": "control.contract_test_selected", "relation": "enables"}, + {"id": "E29", "source": "control.shell_straight_line", "target": "control.contract_test_excluded", "relation": "may_reveal"}, + {"id": "E30", "source": "control.contract_test_excluded", "target": "risk.false_ci_pass", "relation": "would_create_without_guard"}, + {"id": "E31", "source": "risk.false_ci_pass", "target": "outcome.drift", "relation": "is_prevented_by_fail_closed_result"}, + {"id": "E32", "source": "control.contract_test_selected", "target": "evidence.same_bytes_hashed", "relation": "enables_evidence"}, + {"id": "E33", "source": "evidence.same_bytes_hashed", "target": "outcome.pass", "relation": "supports"}, + {"id": "E34", "source": "outcome.pass", "target": "authority.advisory_only", "relation": "remains_bounded_by"} + ], + "coverage_policy": { + "required_node_types": ["control", "evidence", "outcome"], + "route_file": "test-paths-v0.1.json", + "new_blocker_rule": "Every new blocker must add or reuse a causal node and at least one executable route." + } +} diff --git a/standards/lotus-family/causality/test-paths-v0.1.json b/standards/lotus-family/causality/test-paths-v0.1.json new file mode 100644 index 00000000..d000aa7a --- /dev/null +++ b/standards/lotus-family/causality/test-paths-v0.1.json @@ -0,0 +1,125 @@ +{ + "schema_version": "pythia.lotus_causal_test_paths.v0.1", + "graph": "lotus-family-causality-v0.1.json", + "routes": [ + { + "id": "CML-PASS-001", + "repository_id": "cml", + "scenario": {"kind": "valid"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_runnable", "control.step_enabled", "control.direct_run", "control.test_env_clean", "control.shell_straight_line", "control.contract_test_selected", "evidence.same_bytes_hashed", "outcome.pass", "authority.advisory_only"], + "expected": {"outcome": "PASS", "reason_code": "LOTUS_CONTRACT_CONFORMANT"} + }, + { + "id": "LS-PASS-001", + "repository_id": "ls", + "scenario": {"kind": "valid"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_runnable", "control.step_enabled", "control.direct_run", "control.test_env_clean", "control.shell_straight_line", "control.contract_test_selected", "evidence.same_bytes_hashed", "outcome.pass", "authority.advisory_only"], + "expected": {"outcome": "PASS", "reason_code": "LOTUS_CONTRACT_CONFORMANT"} + }, + { + "id": "PYTHIA-PASS-001", + "repository_id": "pythia", + "scenario": {"kind": "valid"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_runnable", "control.step_enabled", "control.direct_run", "control.test_env_clean", "control.shell_straight_line", "control.contract_test_selected", "evidence.same_bytes_hashed", "outcome.pass", "authority.advisory_only"], + "expected": {"outcome": "PASS", "reason_code": "LOTUS_CONTRACT_CONFORMANT"} + }, + { + "id": "CI-NONRUN-001", + "repository_id": "cml", + "scenario": {"kind": "workflow", "workflow": "name: CI\nenv:\n NOTE: |\n python -m pytest\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_runnable", "control.step_enabled", "control.non_run_text", "risk.false_ci_pass", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "CI-STEP-SKIP-001", + "repository_id": "cml", + "scenario": {"kind": "workflow", "workflow": "name: CI\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - name: Run tests\n if: ${{ false }}\n run: |\n python -m pytest\n"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_runnable", "control.step_skipped", "risk.false_ci_pass", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "CI-JOB-SKIP-001", + "repository_id": "cml", + "scenario": {"kind": "workflow", "workflow": "name: CI\njobs:\n test:\n runs-on: ubuntu-latest\n if: ${{ false }}\n steps:\n - name: Run tests\n run: |\n python -m pytest\n"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_not_runnable", "risk.false_ci_pass", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "CI-NO-RUNNER-001", + "repository_id": "cml", + "scenario": {"kind": "workflow", "workflow": "name: CI\njobs:\n test:\n steps:\n - name: Run tests\n run: |\n python -m pytest\n"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_not_runnable", "risk.false_ci_pass", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "CI-QUOTED-ENV-001", + "repository_id": "cml", + "scenario": {"kind": "workflow", "workflow": "name: CI\nenv:\n \"PYTEST_ADDOPTS\": \"--ignore=tests/test_lotus_docs_contract.py\"\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: python -m pytest\n"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_runnable", "control.step_enabled", "control.direct_run", "control.test_env_override", "risk.false_ci_pass", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "CI-SHELL-CONTROL-001", + "repository_id": "cml", + "scenario": {"kind": "workflow", "workflow": "name: CI\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - name: Run tests\n run: |\n if false; then\n python -m pytest\n fi\n"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_runnable", "control.step_enabled", "control.direct_run", "control.test_env_clean", "control.shell_control_flow", "risk.false_ci_pass", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "CML-SUBSET-001", + "repository_id": "cml", + "scenario": {"kind": "workflow", "workflow": "name: CI\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - name: Run tests\n run: |\n python -m pytest tests/test_other.py\n"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_runnable", "control.step_enabled", "control.direct_run", "control.test_env_clean", "control.shell_straight_line", "control.contract_test_excluded", "risk.false_ci_pass", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "PYTHIA-SUBSET-001", + "repository_id": "pythia", + "scenario": {"kind": "workflow", "workflow": "name: CI\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - name: Run tests\n run: |\n mix test test/unrelated_test.exs\n"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_runnable", "control.step_enabled", "control.direct_run", "control.test_env_clean", "control.shell_straight_line", "control.contract_test_excluded", "risk.false_ci_pass", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "CI-FAKE-STEPS-001", + "repository_id": "cml", + "scenario": {"kind": "workflow", "workflow": "name: CI\nmetadata:\n fake-job:\n steps:\n - run: python -m pytest\njobs: {}\n"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_present", "control.workflow_job_not_runnable", "risk.false_ci_pass", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "IDENTITY-INVALID-001", + "repository_id": "cml", + "scenario": {"kind": "invalid_commit", "commit_sha": "main"}, + "path": ["input.snapshot_present", "input.identity_claims_invalid", "outcome.unknown"], + "expected": {"outcome": "UNKNOWN", "reason_code": "COMMIT_SHA_INVALID"} + }, + { + "id": "SNAPSHOT-MISSING-001", + "repository_id": "cml", + "scenario": {"kind": "missing_snapshot"}, + "path": ["input.snapshot_missing", "outcome.unknown"], + "expected": {"outcome": "UNKNOWN", "reason_code": "SNAPSHOT_UNAVAILABLE"} + }, + { + "id": "CONTRACT-DRIFT-001", + "repository_id": "cml", + "scenario": {"kind": "missing_term"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "control.manifest_valid", "control.contract_terms_missing", "outcome.drift"], + "expected": {"outcome": "DRIFT", "reason_code": "LOTUS_CONTRACT_DRIFT"} + }, + { + "id": "IDENTITY-LIMIT-001", + "repository_id": "cml", + "scenario": {"kind": "valid", "assert_identity_mode": "caller_claim_only"}, + "path": ["input.snapshot_present", "input.identity_claims_well_formed", "limitation.identity_unverified", "risk.identity_overclaim", "authority.advisory_only"], + "expected": {"outcome": "PASS", "reason_code": "LOTUS_CONTRACT_CONFORMANT"} + }, + { + "id": "MANIFEST-INVALID-001", + "repository_id": "cml", + "scenario": {"kind": "invalid_manifest"}, + "path": ["control.manifest_invalid", "outcome.unknown"], + "expected": {"outcome": "UNKNOWN", "reason_code": "MANIFEST_INVALID"} + } + ] +} diff --git a/standards/lotus-family/conformance/lotus_family_auditor.py b/standards/lotus-family/conformance/lotus_family_auditor.py index 32cce56d..e039309e 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor.py +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -1,382 +1,7 @@ -from __future__ import annotations - -import json -import re -import shlex -from typing import Any, Mapping - -import lotus_family_auditor_core as _core -from lotus_family_auditor_core import * # noqa: F403 - -_YAML_KEY = re.compile(r"([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)\Z") -_BLOCK_SCALAR = re.compile(r"([|>])(?:[1-9][+-]?|[+-][1-9]?)?\Z") -_ENV_ASSIGNMENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*=.*\Z") -_SHELL_CONTROL = {"&&", "||", ";", "|", "&"} - - -def _strip_unquoted_comment(line: str) -> str: - in_single = False - in_double = False - escaped = False - for index, character in enumerate(line): - if escaped: - escaped = False - continue - if character == "\\" and not in_single: - escaped = True - continue - if character == "'" and not in_double: - in_single = not in_single - continue - if character == '"' and not in_single: - in_double = not in_double - continue - if character == "#" and not in_single and not in_double: - return line[:index] - return line - - -def _indent_of(line: str) -> int | None: - prefix = line[: len(line) - len(line.lstrip(" \t"))] - if "\t" in prefix: - return None - return len(prefix) - - -def _yaml_header(line: str) -> tuple[int, bool, int, str, str] | None: - indent = _indent_of(line) - if indent is None: - return None - stripped = line[indent:] - if not stripped or stripped.startswith("#"): - return None - - is_list = False - key_indent = indent - if stripped.startswith("-"): - match = re.match(r"-([ ]+)(.*)\Z", stripped) - if match is None: - return None - is_list = True - key_indent = indent + 1 + len(match.group(1)) - stripped = match.group(2) - match = _YAML_KEY.fullmatch(stripped) - if match is None: - return None - return indent, is_list, key_indent, match.group(1), match.group(2) - - -def _scalar_indicator(value: str) -> str | None: - normalized = _strip_unquoted_comment(value).strip() - match = _BLOCK_SCALAR.fullmatch(normalized) - return match.group(1) if match is not None else None - - -def _block_scalar_end(lines: list[str], start: int, key_indent: int) -> int: - index = start + 1 - while index < len(lines): - line = lines[index] - if not line.strip(): - index += 1 - continue - indent = _indent_of(line) - if indent is None or indent <= key_indent: - break - index += 1 - return index - - -def _block_scalar_text( - lines: list[str], start: int, end: int, style: str -) -> str: - body = lines[start + 1 : end] - indents = [ - indent - for line in body - if line.strip() and (indent := _indent_of(line)) is not None - ] - if not indents: - return "" - content_indent = min(indents) - deindented = [line[content_indent:] if line.strip() else "" for line in body] - if style == "|": - return "\n".join(deindented) + "\n" - - paragraphs: list[str] = [] - current: list[str] = [] - for line in deindented: - if line: - current.append(line) - elif current: - paragraphs.append(" ".join(current)) - current = [] - elif paragraphs: - paragraphs.append("") - if current: - paragraphs.append(" ".join(current)) - return "\n".join(paragraphs) + ("\n" if paragraphs else "") - - -def _inline_yaml_scalar(value: str) -> str | None: - text = _strip_unquoted_comment(value).strip() - if not text or text in {"null", "Null", "NULL", "~"}: - return None - if text.startswith("'"): - if len(text) < 2 or not text.endswith("'"): - return None - return text[1:-1].replace("''", "'") - if text.startswith('"'): - if len(text) < 2 or not text.endswith('"'): - return None - try: - decoded = json.loads(text) - except json.JSONDecodeError: - return None - return decoded if isinstance(decoded, str) else None - return text - - -def _scalar_ranges(lines: list[str]) -> dict[int, tuple[int, str]]: - ranges: dict[int, tuple[int, str]] = {} - index = 0 - while index < len(lines): - header = _yaml_header(lines[index]) - if header is None: - index += 1 - continue - style = _scalar_indicator(header[4]) - if style is None: - index += 1 - continue - end = _block_scalar_end(lines, index, header[2]) - ranges[index] = (end, style) - index = end - return ranges - - - -def _github_actions_steps_indices( - lines: list[str], scalar_body_lines: set[int] -) -> set[int]: - stack: list[tuple[int, str]] = [] - valid: set[int] = set() - for index, line in enumerate(lines): - if index in scalar_body_lines: - continue - header = _yaml_header(line) - if header is None or header[1]: - continue - _, _, key_indent, key, value = header - while stack and stack[-1][0] >= key_indent: - stack.pop() - parent_keys = [entry[1] for entry in stack] - if ( - key == "steps" - and _inline_yaml_scalar(value) is None - and len(parent_keys) == 2 - and parent_keys[0] == "jobs" - ): - valid.add(index) - if _inline_yaml_scalar(value) is None and _scalar_indicator(value) is None: - stack.append((key_indent, key)) - return valid - -def _github_actions_run_scripts(text: str) -> list[str]: - lines = text.splitlines() - scalar_ranges = _scalar_ranges(lines) - scalar_body_lines = { - line_index - for start, (end, _) in scalar_ranges.items() - for line_index in range(start + 1, end) - } - valid_steps = _github_actions_steps_indices(lines, scalar_body_lines) - scripts: list[str] = [] - index = 0 - - while index < len(lines): - if index in scalar_body_lines: - index += 1 - continue - header = _yaml_header(lines[index]) - if index not in valid_steps or header is None: - index += 1 - continue - - steps_indent = header[2] - block_end = index + 1 - while block_end < len(lines): - line = lines[block_end] - if not line.strip() or line.lstrip().startswith("#"): - block_end += 1 - continue - indent = _indent_of(line) - if indent is None or indent <= steps_indent: - break - block_end += 1 - - item_indices: list[int] = [] - item_indent: int | None = None - for cursor in range(index + 1, block_end): - if cursor in scalar_body_lines: - continue - indent = _indent_of(lines[cursor]) - stripped = lines[cursor][indent:] if indent is not None else "" - if indent is not None and stripped.startswith("-"): - if item_indent is None: - item_indent = indent - if indent == item_indent: - item_indices.append(cursor) - - for position, item_start in enumerate(item_indices): - item_end = ( - item_indices[position + 1] - if position + 1 < len(item_indices) - else block_end - ) - item_header = _yaml_header(lines[item_start]) - if item_header is not None and item_header[1]: - direct_indent = item_header[2] - else: - candidates = [ - candidate[2] - for row in range(item_start + 1, item_end) - if row not in scalar_body_lines - and (candidate := _yaml_header(lines[row])) is not None - and not candidate[1] - ] - if not candidates: - continue - direct_indent = min(candidates) - - for row in range(item_start, item_end): - if row in scalar_body_lines: - continue - row_header = _yaml_header(lines[row]) - if row_header is None: - continue - _, is_list, key_indent, key, value = row_header - if key != "run" or key_indent != direct_indent: - continue - if row != item_start and is_list: - continue - scalar = scalar_ranges.get(row) - if scalar is not None: - end, style = scalar - scripts.append(_block_scalar_text(lines, row, end, style)) - else: - inline = _inline_yaml_scalar(value) - if inline is not None: - scripts.append(inline) - break - index = block_end - return scripts - - -def _yaml_env_names(text: str) -> set[str]: - lines = text.splitlines() - scalar_ranges = _scalar_ranges(lines) - scalar_body_lines = { - line_index - for start, (end, _) in scalar_ranges.items() - for line_index in range(start + 1, end) - } - names: set[str] = set() - for index, line in enumerate(lines): - if index in scalar_body_lines: - continue - header = _yaml_header(line) - if header is None or header[3] != "env": - continue - env_indent = header[2] - inline = _strip_unquoted_comment(header[4]).strip() - if inline: - names.update(re.findall(r"\bPYTEST_[A-Za-z0-9_]*\b", inline)) - continue - for cursor in range(index + 1, len(lines)): - if cursor in scalar_body_lines: - continue - candidate = lines[cursor] - if not candidate.strip() or candidate.lstrip().startswith("#"): - continue - candidate_indent = _indent_of(candidate) - if candidate_indent is None or candidate_indent <= env_indent: - break - child = _yaml_header(candidate) - if child is not None and not child[1] and child[2] > env_indent: - names.add(child[3]) - return names - - -def _has_forbidden_pytest_environment(text: str, scripts: list[str]) -> bool: - if any(name.startswith("PYTEST_") for name in _yaml_env_names(text)): - return True - for script in scripts: - executable = "\n".join( - _strip_unquoted_comment(line) for line in script.splitlines() - ) - if re.search(r"\bPYTEST_[A-Za-z0-9_]*\b", executable): - return True - return False - - -def _tokenize_executed_command(command: str) -> list[str] | None: - try: - lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") - lexer.whitespace_split = True - lexer.commenters = "" - tokens = list(lexer) - except ValueError: - return None - if not tokens or any(token in _SHELL_CONTROL for token in tokens): - return None - if _ENV_ASSIGNMENT.fullmatch(tokens[0]): - return None - return tokens - - -def _ci_discovery( - discovery: Mapping[str, Any], workflow_text: str | list[str] -) -> tuple[bool, list[str]]: - if isinstance(workflow_text, list): - workflow_text = "\n".join(workflow_text) - scripts = _github_actions_run_scripts(workflow_text) - commands = [ - command for script in scripts for command in _core._shell_commands(script) - ] - strategy = discovery["strategy"] - uses_pytest = strategy == "pytest_default_discovery" or any( - str(pattern).endswith(".py") for pattern in discovery.get("contains_any", []) - ) - if uses_pytest and _has_forbidden_pytest_environment(workflow_text, scripts): - return False, [] - - if strategy == "pytest_default_discovery": - test_path = str(discovery["test_path"]) - matched = any( - _core._is_pytest_default_discovery(command, test_path) - for command in commands - ) - return matched, [str(discovery["command"])] if matched else [] - - matches: list[str] = [] - for pattern_value in discovery["contains_any"]: - pattern = str(pattern_value) - if pattern.endswith(".py"): - if any( - _core._is_explicit_pytest_test_command(command, pattern) - for command in commands - ): - matches.append(pattern) - elif any(_core._is_command_prefix(command, pattern) for command in commands): - matches.append(pattern) - return bool(matches), matches - - -_core._tokenize_executed_command = _tokenize_executed_command -_core._ci_discovery = _ci_discovery -if "--cov-fail-under=" not in _core._SAFE_PYTEST_PREFIXES: - _core._SAFE_PYTEST_PREFIXES += ("--cov-fail-under=",) +from lotus_family_runtime import * # noqa: F403 +from lotus_family_schema import DRIFT, PASS, UNKNOWN, load_manifest +from lotus_family_runtime import main if __name__ == "__main__": - raise SystemExit(_core.main()) + raise SystemExit(main()) diff --git a/standards/lotus-family/conformance/lotus_family_auditor_core.py b/standards/lotus-family/conformance/lotus_family_auditor_core.py index 44d528d0..1a1760cf 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor_core.py +++ b/standards/lotus-family/conformance/lotus_family_auditor_core.py @@ -1,707 +1,2 @@ -from __future__ import annotations - -import argparse -import fnmatch -import hashlib -import json -import re -import shlex -from pathlib import Path, PurePosixPath -from typing import Any, Mapping - -PASS = "PASS" -DRIFT = "DRIFT" -UNKNOWN = "UNKNOWN" - -_COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") -_PYTEST_STARTS = (("python", "-m", "pytest"), ("pytest",)) -_SAFE_PYTEST_FLAGS = { - "-q", "--quiet", "-v", "--verbose", "--strict-markers", - "--strict-config", "--disable-warnings", -} -_SAFE_PYTEST_PREFIXES = ( - "--junitxml=", "--cov=", "--cov-report=", "--color=", "--tb=", - "--durations=", "--maxfail=", -) -_FORBIDDEN_PYTEST_FLAGS = { - "--collect-only", "--co", "--setup-only", "--pyargs", - "-k", "--keyword", "-m", "--markers", "--deselect", -} -_SHELL_CONTROL = {"&&", "||", ";", "|", "&"} - - -def load_json_object(path: Path) -> dict[str, Any]: - value = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(value, dict): - raise ValueError(f"{path} root must be an object") - return value - - -def _non_empty_string(value: Any, field: str) -> str: - if not isinstance(value, str) or not value.strip(): - raise ValueError(f"{field} must be a non-empty string") - return value - - -def _relative_manifest_path(value: Any, field: str) -> str: - text = _non_empty_string(value, field) - if "\\" in text: - raise ValueError(f"{field} must use repository-style '/' separators") - path = PurePosixPath(text) - if path.is_absolute() or path == PurePosixPath(".") or ".." in path.parts: - raise ValueError(f"{field} must stay inside the repository snapshot") - return path.as_posix() - - -def _validate_manifest(manifest: Mapping[str, Any]) -> None: - if manifest.get("schema_version") != "pythia.lotus_family_manifest.v0.1": - raise ValueError("unsupported Lotus Family manifest schema") - if manifest.get("authority") != "audit_only": - raise ValueError("Lotus Family manifest authority must be audit_only") - repositories = manifest.get("repositories") - if not isinstance(repositories, list) or not repositories: - raise ValueError("manifest repositories must be a non-empty list") - - ids: set[str] = set() - dirs: set[str] = set() - for repo_index, repo in enumerate(repositories): - prefix = f"repositories[{repo_index}]" - if not isinstance(repo, Mapping): - raise ValueError(f"{prefix} must be an object") - repo_id = _non_empty_string(repo.get("id"), f"{prefix}.id") - if repo_id in ids: - raise ValueError(f"duplicate repository id: {repo_id}") - ids.add(repo_id) - _non_empty_string(repo.get("repository"), f"{prefix}.repository") - snapshot_dir = _relative_manifest_path( - repo.get("snapshot_dir"), f"{prefix}.snapshot_dir" - ) - if "/" in snapshot_dir: - raise ValueError(f"{prefix}.snapshot_dir must be one directory name") - if snapshot_dir in dirs: - raise ValueError(f"duplicate snapshot_dir: {snapshot_dir}") - dirs.add(snapshot_dir) - - checks = repo.get("file_checks") - if not isinstance(checks, list) or not checks: - raise ValueError(f"{prefix}.file_checks must be a non-empty list") - check_ids: set[str] = set() - checked_paths: set[str] = set() - for check_index, check in enumerate(checks): - check_prefix = f"{prefix}.file_checks[{check_index}]" - if not isinstance(check, Mapping): - raise ValueError(f"{check_prefix} must be an object") - check_id = _non_empty_string(check.get("id"), f"{check_prefix}.id") - if check_id in check_ids: - raise ValueError(f"duplicate check id in {repo_id}: {check_id}") - check_ids.add(check_id) - checked_paths.add( - _relative_manifest_path(check.get("path"), f"{check_prefix}.path") - ) - terms = check.get("contains_all") - if not isinstance(terms, list) or not terms: - raise ValueError( - f"{check_prefix}.contains_all must be a non-empty list" - ) - for term_index, term in enumerate(terms): - _non_empty_string( - term, f"{check_prefix}.contains_all[{term_index}]" - ) - - discovery = repo.get("ci_discovery") - if not isinstance(discovery, Mapping): - raise ValueError(f"{prefix}.ci_discovery must be an object") - workflows = discovery.get("workflow_paths") - if not isinstance(workflows, list) or not workflows: - raise ValueError( - f"{prefix}.ci_discovery.workflow_paths must be a non-empty list" - ) - for path_index, workflow_path in enumerate(workflows): - _relative_manifest_path( - workflow_path, - f"{prefix}.ci_discovery.workflow_paths[{path_index}]", - ) - - strategy = discovery.get("strategy") - if strategy == "contains_any": - patterns = discovery.get("contains_any") - if not isinstance(patterns, list) or not patterns: - raise ValueError( - f"{prefix}.ci_discovery.contains_any must be a non-empty list" - ) - for pattern_index, pattern in enumerate(patterns): - _non_empty_string( - pattern, - f"{prefix}.ci_discovery.contains_any[{pattern_index}]", - ) - elif strategy == "pytest_default_discovery": - command = _non_empty_string( - discovery.get("command"), f"{prefix}.ci_discovery.command" - ) - if command != "python -m pytest": - raise ValueError( - f"{prefix}.ci_discovery.command must be 'python -m pytest'" - ) - test_path = _relative_manifest_path( - discovery.get("test_path"), f"{prefix}.ci_discovery.test_path" - ) - test_name = PurePosixPath(test_path).name - if not (test_name.startswith("test_") and test_name.endswith(".py")): - raise ValueError( - f"{prefix}.ci_discovery.test_path is not pytest-discoverable" - ) - if test_path not in checked_paths: - raise ValueError( - f"{prefix}.ci_discovery.test_path must also be a checked file" - ) - else: - raise ValueError( - f"{prefix}.ci_discovery.strategy is unsupported: {strategy}" - ) - - -def load_manifest(path: Path) -> dict[str, Any]: - manifest = load_json_object(path) - _validate_manifest(manifest) - return manifest - - -def _sha256(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def _result( - *, - outcome: str, - reason_code: str, - detail: str, - repository: str, - repository_id: str, - repository_ref: str, - commit_sha: str, - manifest_schema: str, - checks: list[dict[str, Any]], - files: list[dict[str, str]], -) -> dict[str, Any]: - return { - "schema_version": "pythia.lotus_family_audit_result.v0.1", - "outcome": outcome, - "reason_code": reason_code, - "detail": detail, - "repository": repository, - "repository_id": repository_id, - "repository_ref": repository_ref, - "commit_sha": commit_sha, - "manifest_schema": manifest_schema, - "checks": checks, - "files": sorted(files, key=lambda row: row["path"]), - "authority": { - "mode": "audit_only", - "grants_ownership": False, - "grants_approval": False, - "grants_execution": False, - "grants_delivery": False, - "grants_merge": False, - }, - } - - -def _manifest_invalid_result( - detail: str, - repository_id: str, - repository_ref: str, - commit_sha: str, - manifest_schema: str, -) -> dict[str, Any]: - return _result( - outcome=UNKNOWN, - reason_code="MANIFEST_INVALID", - detail=detail, - repository="", - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - - -def _repository_config( - manifest: Mapping[str, Any], repository_id: str -) -> Mapping[str, Any] | None: - return next( - (row for row in manifest["repositories"] if row.get("id") == repository_id), - None, - ) - - -def _contained_path( - root: Path, relative_path: str -) -> tuple[Path | None, str | None]: - try: - normalized = _relative_manifest_path(relative_path, "manifest path") - root_resolved = root.resolve() - path = (root_resolved / Path(*PurePosixPath(normalized).parts)).resolve() - path.relative_to(root_resolved) - return path, None - except (OSError, ValueError): - return None, f"path escapes repository snapshot: {relative_path}" - - -def _read_file( - root: Path, - relative_path: str, - files: list[dict[str, str]], -) -> tuple[str | None, str | None]: - path, error = _contained_path(root, relative_path) - if error is not None or path is None: - return None, error - if not path.is_file(): - return None, f"required file is missing: {relative_path}" - try: - text = path.read_text(encoding="utf-8") - files.append({"path": relative_path, "sha256": _sha256(path)}) - return text, None - except (OSError, UnicodeError) as exc: - return None, f"cannot read {relative_path}: {exc.__class__.__name__}" - - -def _strip_unquoted_shell_comment(line: str) -> str: - in_single = False - in_double = False - escaped = False - for index, character in enumerate(line): - if escaped: - escaped = False - continue - if character == "\\" and not in_single: - escaped = True - continue - if character == "'" and not in_double: - in_single = not in_single - continue - if character == '"' and not in_single: - in_double = not in_double - continue - if character == "#" and not in_single and not in_double: - return line[:index] - return line - - -def _shell_commands(text: str) -> list[str]: - lines = text.splitlines() - commands: list[str] = [] - index = 0 - while index < len(lines): - first = _strip_unquoted_shell_comment(lines[index]).strip() - if not first: - index += 1 - continue - parts = [first] - while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): - parts[-1] = parts[-1].rstrip()[:-1] - index += 1 - continuation = _strip_unquoted_shell_comment(lines[index]).strip() - if continuation: - parts.append(continuation) - commands.append(" ".join(parts)) - index += 1 - return commands - - -def _tokenize_executed_command(command: str) -> list[str] | None: - try: - tokens = shlex.split(command) - except ValueError: - return None - if not tokens or any(token in _SHELL_CONTROL for token in tokens): - return None - index = 0 - while index < len(tokens) and re.fullmatch( - r"[A-Za-z_][A-Za-z0-9_]*=.*", tokens[index] - ): - index += 1 - return tokens[index:] or None - - -def _pytest_parts(tokens: list[str]) -> tuple[list[str], list[str]] | None: - for start in _PYTEST_STARTS: - if tuple(tokens[: len(start)]) == start: - return list(start), tokens[len(start) :] - return None - - -def _normalize_repo_pattern(value: str) -> str: - normalized = value.strip().replace("\\", "/") - while normalized.startswith("./"): - normalized = normalized[2:] - return normalized.rstrip("/") - - -def _ignore_path_covers_test(ignore_path: str, test_path: str) -> bool: - normalized = _normalize_repo_pattern(ignore_path) - if not normalized: - return True - ignored = PurePosixPath(normalized) - test = PurePosixPath(test_path) - if ignored.is_absolute() or ".." in ignored.parts: - return True - return ignored == test or ignored in test.parents - - -def _ignore_glob_covers_test(pattern: str, test_path: str) -> bool: - normalized = _normalize_repo_pattern(pattern) - if not normalized: - return True - test = PurePosixPath(test_path) - candidates = [test.as_posix(), test.name] - candidates.extend( - parent.as_posix() - for parent in test.parents - if parent != PurePosixPath(".") - ) - return any( - fnmatch.fnmatchcase(candidate, normalized) for candidate in candidates - ) - - -def _pytest_arguments_safe( - arguments: list[str], test_path: str, *, require_test_path: bool -) -> bool: - positive_paths: list[str] = [] - index = 0 - while index < len(arguments): - argument = arguments[index] - if argument in _FORBIDDEN_PYTEST_FLAGS or argument.startswith( - ("-k=", "--keyword=", "-m=", "--markers=", "--deselect=") - ): - return False - if argument.startswith("--ignore="): - if _ignore_path_covers_test(argument.split("=", 1)[1], test_path): - return False - elif argument == "--ignore": - if index + 1 >= len(arguments): - return False - if _ignore_path_covers_test(arguments[index + 1], test_path): - return False - index += 1 - elif argument.startswith("--ignore-glob="): - if _ignore_glob_covers_test(argument.split("=", 1)[1], test_path): - return False - elif argument == "--ignore-glob": - if index + 1 >= len(arguments): - return False - if _ignore_glob_covers_test(arguments[index + 1], test_path): - return False - index += 1 - elif argument in _SAFE_PYTEST_FLAGS or argument.startswith( - _SAFE_PYTEST_PREFIXES - ): - pass - elif argument.startswith("-"): - return False - else: - positive_paths.append( - _normalize_repo_pattern(argument.split("::", 1)[0]) - ) - index += 1 - - normalized_test = _normalize_repo_pattern(test_path) - if require_test_path: - return normalized_test in positive_paths - return not positive_paths - - -def _is_pytest_default_discovery(command: str, test_path: str) -> bool: - tokens = _tokenize_executed_command(command) - if tokens is None: - return False - parts = _pytest_parts(tokens) - return parts is not None and _pytest_arguments_safe( - parts[1], test_path, require_test_path=False - ) - - -def _is_explicit_pytest_test_command(command: str, test_path: str) -> bool: - tokens = _tokenize_executed_command(command) - if tokens is None: - return False - parts = _pytest_parts(tokens) - return parts is not None and _pytest_arguments_safe( - parts[1], test_path, require_test_path=True - ) - - -def _is_command_prefix(command: str, prefix: str) -> bool: - tokens = _tokenize_executed_command(command) - if tokens is None: - return False - try: - required = shlex.split(prefix) - except ValueError: - return False - return tokens[: len(required)] == required - - -def _ci_discovery( - discovery: Mapping[str, Any], combined: str -) -> tuple[bool, list[str]]: - commands = _shell_commands(combined) - strategy = discovery["strategy"] - if strategy == "pytest_default_discovery": - test_path = str(discovery["test_path"]) - matched = any( - _is_pytest_default_discovery(command, test_path) - for command in commands - ) - return matched, [str(discovery["command"])] if matched else [] - - matches: list[str] = [] - for pattern_value in discovery["contains_any"]: - pattern = str(pattern_value) - if pattern.endswith(".py"): - if any( - _is_explicit_pytest_test_command(command, pattern) - for command in commands - ): - matches.append(pattern) - elif any(_is_command_prefix(command, pattern) for command in commands): - matches.append(pattern) - return bool(matches), matches - - -def audit_repository( - manifest: Mapping[str, Any], - *, - repository_id: str, - snapshot_root: Path, - repository_ref: str, - commit_sha: str, -) -> dict[str, Any]: - manifest_schema = str(manifest.get("schema_version", "")) - try: - _validate_manifest(manifest) - except (TypeError, ValueError) as exc: - return _manifest_invalid_result( - str(exc), - repository_id, - repository_ref, - commit_sha, - manifest_schema, - ) - - config = _repository_config(manifest, repository_id) - if config is None: - return _result( - outcome=UNKNOWN, - reason_code="REPOSITORY_NOT_CONFIGURED", - detail=f"repository adapter is not configured: {repository_id}", - repository="", - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - - repository = str(config["repository"]) - if not repository_ref: - return _result( - outcome=UNKNOWN, - reason_code="REPOSITORY_REF_MISSING", - detail="an exact repository ref is required", - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - if not _COMMIT_SHA.fullmatch(commit_sha): - return _result( - outcome=UNKNOWN, - reason_code="COMMIT_SHA_INVALID", - detail="commit_sha must be a lowercase 40-character hexadecimal SHA", - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - - snapshot_root_resolved = snapshot_root.resolve() - repository_root = ( - snapshot_root_resolved / str(config["snapshot_dir"]) - ).resolve() - try: - repository_root.relative_to(snapshot_root_resolved) - except ValueError: - return _result( - outcome=UNKNOWN, - reason_code="SNAPSHOT_UNAVAILABLE", - detail="repository snapshot escapes snapshot_root", - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - if not repository_root.is_dir(): - return _result( - outcome=UNKNOWN, - reason_code="SNAPSHOT_UNAVAILABLE", - detail=f"repository snapshot is unavailable: {config['snapshot_dir']}", - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=[], - files=[], - ) - - checks: list[dict[str, Any]] = [] - files: list[dict[str, str]] = [] - drift: list[str] = [] - for check in config["file_checks"]: - check_id = str(check["id"]) - relative_path = str(check["path"]) - text, error = _read_file(repository_root, relative_path, files) - if error is not None or text is None: - checks.append( - { - "check_id": check_id, - "outcome": DRIFT, - "path": relative_path, - "missing_terms": [], - "detail": error, - } - ) - drift.append(check_id) - continue - terms = [str(term) for term in check["contains_all"]] - missing_terms = [term for term in terms if term not in text] - outcome = PASS if not missing_terms else DRIFT - checks.append( - { - "check_id": check_id, - "outcome": outcome, - "path": relative_path, - "missing_terms": missing_terms, - "detail": ( - "all required terms are present" - if outcome == PASS - else "required terms are missing" - ), - } - ) - if outcome == DRIFT: - drift.append(check_id) - - discovery = config["ci_discovery"] - workflow_paths = [str(path) for path in discovery["workflow_paths"]] - workflow_texts: list[str] = [] - read_errors: list[str] = [] - for relative_path in workflow_paths: - text, error = _read_file(repository_root, relative_path, files) - if error is not None or text is None: - read_errors.append(error or f"cannot read {relative_path}") - else: - workflow_texts.append(text) - - discovered, matched_patterns = _ci_discovery( - discovery, "\n".join(workflow_texts) - ) - if read_errors and not workflow_texts: - ci_outcome = DRIFT - detail = "; ".join(read_errors) - elif discovered: - ci_outcome = PASS - detail = "contract regression test is discovered by executable CI" - else: - ci_outcome = DRIFT - detail = "no configured executable CI discovery rule matched" - if ci_outcome == DRIFT: - drift.append("ci_discovery") - checks.append( - { - "check_id": "ci_discovery", - "outcome": ci_outcome, - "paths": workflow_paths, - "matched_patterns": matched_patterns, - "detail": detail, - } - ) - - if drift: - outcome = DRIFT - reason_code = "LOTUS_CONTRACT_DRIFT" - detail = "non-conforming checks: " + ", ".join(sorted(set(drift))) - else: - outcome = PASS - reason_code = "LOTUS_CONTRACT_CONFORMANT" - detail = "all configured Lotus Family invariants passed" - - unique_files = {(row["path"], row["sha256"]): row for row in files} - return _result( - outcome=outcome, - reason_code=reason_code, - detail=detail, - repository=repository, - repository_id=repository_id, - repository_ref=repository_ref, - commit_sha=commit_sha, - manifest_schema=manifest_schema, - checks=checks, - files=list(unique_files.values()), - ) - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser( - description="Audit one exact repository snapshot against the Lotus Family manifest." - ) - parser.add_argument("--manifest", type=Path, required=True) - parser.add_argument("--snapshot-root", type=Path, required=True) - parser.add_argument("--repository-id", required=True) - parser.add_argument("--repository-ref", required=True) - parser.add_argument("--commit-sha", required=True) - parser.add_argument("--output", type=Path, required=True) - args = parser.parse_args(argv) - - try: - manifest = load_manifest(args.manifest) - result = audit_repository( - manifest, - repository_id=args.repository_id, - snapshot_root=args.snapshot_root, - repository_ref=args.repository_ref, - commit_sha=args.commit_sha, - ) - except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: - result = _manifest_invalid_result( - str(exc), - args.repository_id, - args.repository_ref, - args.commit_sha, - "", - ) - - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text( - json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - print(json.dumps(result, ensure_ascii=False, sort_keys=True)) - return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(result["outcome"], 3) - - -if __name__ == "__main__": - raise SystemExit(main()) +from lotus_family_runtime import * # noqa: F403 +from lotus_family_schema import DRIFT, PASS, UNKNOWN, load_manifest diff --git a/standards/lotus-family/conformance/lotus_family_runtime.py b/standards/lotus-family/conformance/lotus_family_runtime.py new file mode 100644 index 00000000..e0fd669c --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_runtime.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any, Mapping + +from lotus_family_schema import ( + COMMIT_SHA, + DRIFT, + PASS, + UNKNOWN, + load_manifest, + manifest_invalid, + read_file, + repository_config, + result, + validate_manifest, +) +from lotus_family_workflow import ci_discovery + + +def audit_repository( + manifest: Mapping[str, Any], + *, + repository_id: str, + snapshot_root: Path, + repository_ref: str, + commit_sha: str, +) -> dict[str, Any]: + schema = str(manifest.get("schema_version", "")) + try: + validate_manifest(manifest) + except (TypeError, ValueError) as exc: + return manifest_invalid(str(exc), repository_id, repository_ref, commit_sha, schema) + config = repository_config(manifest, repository_id) + if config is None: + return result(outcome=UNKNOWN, reason_code="REPOSITORY_NOT_CONFIGURED", + detail=f"repository adapter is not configured: {repository_id}", + repository="", repository_id=repository_id, repository_ref=repository_ref, + commit_sha=commit_sha, manifest_schema=schema, checks=[], files=[]) + repository = str(config["repository"]) + if not repository_ref: + return result(outcome=UNKNOWN, reason_code="REPOSITORY_REF_MISSING", + detail="an exact repository ref claim is required", repository=repository, + repository_id=repository_id, repository_ref=repository_ref, commit_sha=commit_sha, + manifest_schema=schema, checks=[], files=[]) + if not COMMIT_SHA.fullmatch(commit_sha): + return result(outcome=UNKNOWN, reason_code="COMMIT_SHA_INVALID", + detail="commit_sha claim must be a lowercase 40-character hexadecimal SHA", + repository=repository, repository_id=repository_id, repository_ref=repository_ref, + commit_sha=commit_sha, manifest_schema=schema, checks=[], files=[]) + root = snapshot_root.resolve() + repository_root = (root / str(config["snapshot_dir"])).resolve() + try: + repository_root.relative_to(root) + except ValueError: + return result(outcome=UNKNOWN, reason_code="SNAPSHOT_UNAVAILABLE", + detail="repository snapshot escapes snapshot_root", repository=repository, + repository_id=repository_id, repository_ref=repository_ref, commit_sha=commit_sha, + manifest_schema=schema, checks=[], files=[]) + if not repository_root.is_dir(): + return result(outcome=UNKNOWN, reason_code="SNAPSHOT_UNAVAILABLE", + detail=f"repository snapshot is unavailable: {config['snapshot_dir']}", + repository=repository, repository_id=repository_id, repository_ref=repository_ref, + commit_sha=commit_sha, manifest_schema=schema, checks=[], files=[]) + + checks: list[dict[str, Any]] = [] + files: list[dict[str, str]] = [] + drift: list[str] = [] + for check in config["file_checks"]: + check_id = str(check["id"]) + name = str(check["path"]) + text, error = read_file(repository_root, name, files) + if error or text is None: + checks.append({"check_id": check_id, "outcome": DRIFT, "path": name, + "missing_terms": [], "detail": error}) + drift.append(check_id) + continue + terms = [str(term) for term in check["contains_all"]] + missing = [term for term in terms if term not in text] + outcome = PASS if not missing else DRIFT + checks.append({"check_id": check_id, "outcome": outcome, "path": name, + "missing_terms": missing, + "detail": "all required terms are present" if outcome == PASS else "required terms are missing"}) + if outcome == DRIFT: + drift.append(check_id) + + discovery = config["ci_discovery"] + workflow_paths = [str(path) for path in discovery["workflow_paths"]] + texts: list[str] = [] + errors: list[str] = [] + for name in workflow_paths: + text, error = read_file(repository_root, name, files) + if error or text is None: + errors.append(error or f"cannot read {name}") + else: + texts.append(text) + discovered, matched = ci_discovery(discovery, texts) + if errors and not texts: + ci_outcome, detail = DRIFT, "; ".join(errors) + elif discovered: + ci_outcome, detail = PASS, "contract regression test is discovered by executable CI" + else: + ci_outcome, detail = DRIFT, "no configured executable CI discovery rule matched" + if ci_outcome == DRIFT: + drift.append("ci_discovery") + checks.append({"check_id": "ci_discovery", "outcome": ci_outcome, + "paths": workflow_paths, "matched_patterns": matched, "detail": detail}) + + if drift: + outcome, reason = DRIFT, "LOTUS_CONTRACT_DRIFT" + detail = "non-conforming checks: " + ", ".join(sorted(set(drift))) + else: + outcome, reason = PASS, "LOTUS_CONTRACT_CONFORMANT" + detail = "all configured Lotus Family invariants passed for the supplied snapshot and caller-provided identity claims" + unique = {(row["path"], row["sha256"]): row for row in files} + return result(outcome=outcome, reason_code=reason, detail=detail, repository=repository, + repository_id=repository_id, repository_ref=repository_ref, commit_sha=commit_sha, + manifest_schema=schema, checks=checks, files=list(unique.values())) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Audit one repository snapshot against the Lotus Family manifest.") + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--snapshot-root", type=Path, required=True) + parser.add_argument("--repository-id", required=True) + parser.add_argument("--repository-ref", required=True) + parser.add_argument("--commit-sha", required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args(argv) + try: + audit = audit_repository(load_manifest(args.manifest), repository_id=args.repository_id, + snapshot_root=args.snapshot_root, repository_ref=args.repository_ref, + commit_sha=args.commit_sha) + except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: + audit = manifest_invalid(str(exc), args.repository_id, args.repository_ref, args.commit_sha, "") + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(audit, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(audit, ensure_ascii=False, sort_keys=True)) + return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(audit["outcome"], 3) diff --git a/standards/lotus-family/conformance/lotus_family_schema.py b/standards/lotus-family/conformance/lotus_family_schema.py new file mode 100644 index 00000000..ac04b070 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_schema.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import Path, PurePosixPath +from typing import Any, Mapping + +PASS, DRIFT, UNKNOWN = "PASS", "DRIFT", "UNKNOWN" +COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") + + +def load_json_object(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"{path} root must be an object") + return value + + +def non_empty(value: Any, field: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field} must be a non-empty string") + return value + + +def relative_path(value: Any, field: str) -> str: + text = non_empty(value, field) + if "\\" in text: + raise ValueError(f"{field} must use repository-style '/' separators") + path = PurePosixPath(text) + if path.is_absolute() or path == PurePosixPath(".") or ".." in path.parts: + raise ValueError(f"{field} must stay inside the repository snapshot") + return path.as_posix() + + +def validate_manifest(manifest: Mapping[str, Any]) -> None: + if manifest.get("schema_version") != "pythia.lotus_family_manifest.v0.1": + raise ValueError("unsupported Lotus Family manifest schema") + if manifest.get("authority") != "audit_only": + raise ValueError("Lotus Family manifest authority must be audit_only") + repos = manifest.get("repositories") + if not isinstance(repos, list) or not repos: + raise ValueError("manifest repositories must be a non-empty list") + ids: set[str] = set() + dirs: set[str] = set() + for ri, repo in enumerate(repos): + prefix = f"repositories[{ri}]" + if not isinstance(repo, Mapping): + raise ValueError(f"{prefix} must be an object") + repo_id = non_empty(repo.get("id"), f"{prefix}.id") + if repo_id in ids: + raise ValueError(f"duplicate repository id: {repo_id}") + ids.add(repo_id) + non_empty(repo.get("repository"), f"{prefix}.repository") + snapshot_dir = relative_path(repo.get("snapshot_dir"), f"{prefix}.snapshot_dir") + if "/" in snapshot_dir or snapshot_dir in dirs: + raise ValueError(f"invalid or duplicate snapshot_dir: {snapshot_dir}") + dirs.add(snapshot_dir) + checks = repo.get("file_checks") + if not isinstance(checks, list) or not checks: + raise ValueError(f"{prefix}.file_checks must be a non-empty list") + check_ids: set[str] = set() + checked: set[str] = set() + for ci, check in enumerate(checks): + cp = f"{prefix}.file_checks[{ci}]" + if not isinstance(check, Mapping): + raise ValueError(f"{cp} must be an object") + check_id = non_empty(check.get("id"), f"{cp}.id") + if check_id in check_ids: + raise ValueError(f"duplicate check id in {repo_id}: {check_id}") + check_ids.add(check_id) + checked.add(relative_path(check.get("path"), f"{cp}.path")) + terms = check.get("contains_all") + if not isinstance(terms, list) or not terms: + raise ValueError(f"{cp}.contains_all must be a non-empty list") + for ti, term in enumerate(terms): + non_empty(term, f"{cp}.contains_all[{ti}]") + discovery = repo.get("ci_discovery") + if not isinstance(discovery, Mapping): + raise ValueError(f"{prefix}.ci_discovery must be an object") + workflows = discovery.get("workflow_paths") + if not isinstance(workflows, list) or not workflows: + raise ValueError(f"{prefix}.ci_discovery.workflow_paths must be a non-empty list") + for wi, workflow in enumerate(workflows): + relative_path(workflow, f"{prefix}.ci_discovery.workflow_paths[{wi}]") + strategy = discovery.get("strategy") + if strategy == "contains_any": + patterns = discovery.get("contains_any") + if not isinstance(patterns, list) or not patterns: + raise ValueError(f"{prefix}.ci_discovery.contains_any must be a non-empty list") + for pi, pattern in enumerate(patterns): + text = non_empty(pattern, f"{prefix}.ci_discovery.contains_any[{pi}]") + if text.endswith((".py", ".exs")) and relative_path(text, f"{prefix}.ci_discovery.contains_any[{pi}]") not in checked: + raise ValueError(f"{prefix}.ci_discovery pattern must also be a checked file") + elif strategy in {"pytest_default_discovery", "mix_default_discovery"}: + expected = "python -m pytest" if strategy.startswith("pytest") else "mix test" + if non_empty(discovery.get("command"), f"{prefix}.ci_discovery.command") != expected: + raise ValueError(f"{prefix}.ci_discovery.command must be {expected!r}") + test_path = relative_path(discovery.get("test_path"), f"{prefix}.ci_discovery.test_path") + name = PurePosixPath(test_path).name + if strategy.startswith("pytest") and not (name.startswith("test_") and name.endswith(".py")): + raise ValueError(f"{prefix}.ci_discovery.test_path is not pytest-discoverable") + if strategy.startswith("mix") and not name.endswith("_test.exs"): + raise ValueError(f"{prefix}.ci_discovery.test_path is not Mix-test-discoverable") + if test_path not in checked: + raise ValueError(f"{prefix}.ci_discovery.test_path must also be a checked file") + else: + raise ValueError(f"{prefix}.ci_discovery.strategy is unsupported: {strategy}") + + +def load_manifest(path: Path) -> dict[str, Any]: + manifest = load_json_object(path) + validate_manifest(manifest) + return manifest + + +def repository_config(manifest: Mapping[str, Any], repository_id: str) -> Mapping[str, Any] | None: + return next((row for row in manifest["repositories"] if row.get("id") == repository_id), None) + + +def contained_path(root: Path, name: str) -> tuple[Path | None, str | None]: + try: + normalized = relative_path(name, "manifest path") + resolved_root = root.resolve() + path = (resolved_root / Path(*PurePosixPath(normalized).parts)).resolve() + path.relative_to(resolved_root) + return path, None + except (OSError, ValueError): + return None, f"path escapes repository snapshot: {name}" + + +def read_file(root: Path, name: str, files: list[dict[str, str]]) -> tuple[str | None, str | None]: + path, error = contained_path(root, name) + if error or path is None: + return None, error + if not path.is_file(): + return None, f"required file is missing: {name}" + try: + data = path.read_bytes() + text = data.decode("utf-8") + files.append({"path": name, "sha256": hashlib.sha256(data).hexdigest()}) + return text, None + except (OSError, UnicodeError) as exc: + return None, f"cannot read {name}: {exc.__class__.__name__}" + + +def result(*, outcome: str, reason_code: str, detail: str, repository: str, repository_id: str, + repository_ref: str, commit_sha: str, manifest_schema: str, + checks: list[dict[str, Any]], files: list[dict[str, str]]) -> dict[str, Any]: + return { + "schema_version": "pythia.lotus_family_audit_result.v0.1", + "outcome": outcome, + "reason_code": reason_code, + "detail": detail, + "repository": repository, + "repository_id": repository_id, + "repository_ref": repository_ref, + "commit_sha": commit_sha, + "identity_assurance": { + "mode": "caller_claim_only", + "remote_repository_verified": False, + "commit_reachability_verified": False, + "working_tree_clean_verified": False, + }, + "manifest_schema": manifest_schema, + "checks": checks, + "files": sorted(files, key=lambda row: row["path"]), + "authority": { + "mode": "audit_only", + "grants_ownership": False, + "grants_approval": False, + "grants_execution": False, + "grants_delivery": False, + "grants_merge": False, + }, + } + + +def manifest_invalid(detail: str, repository_id: str, repository_ref: str, commit_sha: str, schema: str) -> dict[str, Any]: + return result(outcome=UNKNOWN, reason_code="MANIFEST_INVALID", detail=detail, + repository="", repository_id=repository_id, repository_ref=repository_ref, + commit_sha=commit_sha, manifest_schema=schema, checks=[], files=[]) diff --git a/standards/lotus-family/conformance/lotus_family_workflow.py b/standards/lotus-family/conformance/lotus_family_workflow.py new file mode 100644 index 00000000..29a79bf8 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow.py @@ -0,0 +1,427 @@ +from __future__ import annotations + +import fnmatch +import json +import re +import shlex +from pathlib import PurePosixPath +from typing import Any, Mapping + +BARE_KEY = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*\Z") +BLOCK_SCALAR = re.compile(r"([|>])(?:[1-9][+-]?|[+-][1-9]?)?\Z") +ENV_ASSIGNMENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*=.*\Z") +PYTEST_STARTS = (("python", "-m", "pytest"), ("pytest",)) +SAFE_PYTEST_FLAGS = {"-q", "--quiet", "-v", "--verbose", "--strict-markers", "--strict-config", "--disable-warnings"} +SAFE_PYTEST_PREFIXES = ("--junitxml=", "--cov=", "--cov-report=", "--cov-fail-under=", "--color=", "--tb=", "--durations=", "--maxfail=") +FORBIDDEN_PYTEST_FLAGS = {"--collect-only", "--co", "--setup-only", "--pyargs", "-k", "--keyword", "-m", "--markers", "--deselect"} +SAFE_MIX_FLAGS = {"--trace", "--color"} +SAFE_MIX_PREFIXES = ("--seed=", "--max-failures=") +CONTROL_TOKENS = {"&&", "||", ";", "|", "&", "(", ")", "{", "}"} +CONTROL_WORDS = {"if", "then", "elif", "else", "fi", "for", "while", "until", "do", "done", "case", "esac", "select", "function"} + + +def strip_comment(line: str) -> str: + single = double = escaped = False + for index, char in enumerate(line): + if escaped: + escaped = False + elif char == "\\" and not single: + escaped = True + elif char == "'" and not double: + single = not single + elif char == '"' and not single: + double = not double + elif char == "#" and not single and not double: + return line[:index] + return line + + +def indent_of(line: str) -> int | None: + prefix = line[: len(line) - len(line.lstrip(" \t"))] + return None if "\t" in prefix else len(prefix) + + +def decode_key(text: str) -> str | None: + key = text.strip() + if BARE_KEY.fullmatch(key): + return key + if len(key) >= 2 and key[0] == key[-1] == "'": + return key[1:-1].replace("''", "'") or None + if len(key) >= 2 and key[0] == key[-1] == '"': + try: + decoded = json.loads(key) + except json.JSONDecodeError: + return None + return decoded if isinstance(decoded, str) and decoded else None + return None + + +def split_pair(text: str) -> tuple[str, str] | None: + single = double = escaped = False + for index, char in enumerate(text): + if escaped: + escaped = False + elif char == "\\" and double: + escaped = True + elif char == "'" and not double: + single = not single + elif char == '"' and not single: + double = not double + elif char == ":" and not single and not double: + return text[:index], text[index + 1 :] + return None + + +def yaml_header(line: str) -> tuple[int, bool, int, str, str] | None: + indent = indent_of(line) + if indent is None: + return None + text = line[indent:] + if not text or text.startswith("#"): + return None + is_list = False + key_indent = indent + if text.startswith("-"): + match = re.match(r"-([ ]+)(.*)\Z", text) + if not match: + return None + is_list = True + key_indent = indent + 1 + len(match.group(1)) + text = match.group(2) + pair = split_pair(text) + if not pair: + return None + key = decode_key(pair[0]) + return None if key is None else (indent, is_list, key_indent, key, pair[1]) + + +def scalar_indicator(value: str) -> str | None: + match = BLOCK_SCALAR.fullmatch(strip_comment(value).strip()) + return match.group(1) if match else None + + +def inline_scalar(value: str) -> str | None: + text = strip_comment(value).strip() + if not text or text in {"null", "Null", "NULL", "~"}: + return None + if text.startswith("'"): + return text[1:-1].replace("''", "'") if text.endswith("'") else None + if text.startswith('"'): + if not text.endswith('"'): + return None + try: + value = json.loads(text) + except json.JSONDecodeError: + return None + return value if isinstance(value, str) else None + return text + + +def condition_true(value: str) -> bool: + scalar = inline_scalar(value) + return scalar is not None and re.sub(r"\s+", " ", scalar.strip()).lower() in {"true", "${{ true }}"} + + +def scalar_ranges(lines: list[str]) -> dict[int, tuple[int, str]]: + result: dict[int, tuple[int, str]] = {} + index = 0 + while index < len(lines): + header = yaml_header(lines[index]) + style = scalar_indicator(header[4]) if header else None + if not header or not style: + index += 1 + continue + end = index + 1 + while end < len(lines): + if not lines[end].strip(): + end += 1 + continue + indent = indent_of(lines[end]) + if indent is None or indent <= header[2]: + break + end += 1 + result[index] = (end, style) + index = end + return result + + +def scalar_text(lines: list[str], start: int, end: int, style: str) -> str: + body = lines[start + 1 : end] + indents = [indent for line in body if line.strip() and (indent := indent_of(line)) is not None] + if not indents: + return "" + minimum = min(indents) + body = [line[minimum:] if line.strip() else "" for line in body] + if style == "|": + return "\n".join(body) + "\n" + paragraphs: list[str] = [] + current: list[str] = [] + for line in body: + if line: + current.append(line) + elif current: + paragraphs.append(" ".join(current)); current = [] + elif paragraphs: + paragraphs.append("") + if current: + paragraphs.append(" ".join(current)) + return "\n".join(paragraphs) + ("\n" if paragraphs else "") + + +def block_end(lines: list[str], start: int, parent_indent: int) -> int: + index = start + 1 + while index < len(lines): + if not lines[index].strip() or lines[index].lstrip().startswith("#"): + index += 1; continue + indent = indent_of(lines[index]) + if indent is None or indent <= parent_indent: + break + index += 1 + return index + + +def direct_headers(lines: list[str], start: int, end: int, parent_indent: int, scalar_body: set[int]): + found = [] + minimum = None + for row in range(start, end): + if row in scalar_body: + continue + header = yaml_header(lines[row]) + if not header or header[1] or header[2] <= parent_indent: + continue + if minimum is None or header[2] < minimum: + minimum, found = header[2], [(row, header)] + elif header[2] == minimum: + found.append((row, header)) + return found + + +def item_starts(lines: list[str], start: int, end: int, scalar_body: set[int]) -> list[int]: + items: list[int] = [] + minimum = None + for row in range(start, end): + if row in scalar_body: + continue + indent = indent_of(lines[row]) + text = lines[row][indent:] if indent is not None else "" + if indent is not None and text.startswith("-"): + minimum = indent if minimum is None else minimum + if indent == minimum: + items.append(row) + return items + + +def github_run_scripts(text: str) -> list[str]: + lines = text.splitlines() + ranges = scalar_ranges(lines) + scalar_body = {row for start, (end, _) in ranges.items() for row in range(start + 1, end)} + scripts: list[str] = [] + jobs = [(row, h) for row, line in enumerate(lines) if row not in scalar_body and (h := yaml_header(line)) and not h[1] and h[2] == 0 and h[3] == "jobs" and inline_scalar(h[4]) is None and scalar_indicator(h[4]) is None] + for jobs_row, jobs_header in jobs: + jobs_end = block_end(lines, jobs_row, jobs_header[2]) + job_headers = direct_headers(lines, jobs_row + 1, jobs_end, jobs_header[2], scalar_body) + for ji, (job_row, job_header) in enumerate(job_headers): + job_end = job_headers[ji + 1][0] if ji + 1 < len(job_headers) else jobs_end + props = {h[3]: (row, h) for row, h in direct_headers(lines, job_row + 1, job_end, job_header[2], scalar_body)} + runs_on = props.get("runs-on") + if not runs_on or inline_scalar(runs_on[1][4]) is None: + continue + job_if = props.get("if") + if job_if and not condition_true(job_if[1][4]): + continue + steps = props.get("steps") + if not steps or inline_scalar(steps[1][4]) is not None or scalar_indicator(steps[1][4]) is not None: + continue + steps_end = block_end(lines, steps[0], steps[1][2]) + items = item_starts(lines, steps[0] + 1, steps_end, scalar_body) + for si, item_row in enumerate(items): + item_end = items[si + 1] if si + 1 < len(items) else steps_end + first = yaml_header(lines[item_row]) + if not first or not first[1]: + continue + props = {first[3]: (item_row, first)} + for row in range(item_row + 1, item_end): + if row in scalar_body: + continue + header = yaml_header(lines[row]) + if header and not header[1] and header[2] == first[2]: + props[header[3]] = (row, header) + step_if = props.get("if") + if step_if and not condition_true(step_if[1][4]): + continue + run = props.get("run") + if not run: + continue + if run[0] in ranges: + end, style = ranges[run[0]] + scripts.append(scalar_text(lines, run[0], end, style)) + elif (value := inline_scalar(run[1][4])) is not None: + scripts.append(value) + return scripts + + +def yaml_env_names(text: str) -> set[str]: + lines = text.splitlines(); ranges = scalar_ranges(lines) + body = {row for start, (end, _) in ranges.items() for row in range(start + 1, end)} + names: set[str] = set() + for index, line in enumerate(lines): + if index in body: + continue + header = yaml_header(line) + if not header or header[3] != "env": + continue + inline = strip_comment(header[4]).strip() + if inline: + names.update(re.findall(r"\bPYTEST_[A-Za-z0-9_]*\b", inline)); continue + for row in range(index + 1, len(lines)): + if row in body: + continue + if not lines[row].strip() or lines[row].lstrip().startswith("#"): + continue + indent = indent_of(lines[row]) + if indent is None or indent <= header[2]: + break + child = yaml_header(lines[row]) + if child and not child[1] and child[2] > header[2]: + names.add(child[3]) + return names + + +def shell_commands(text: str) -> list[str]: + stripped = "\n".join(strip_comment(line) for line in text.splitlines()) + try: + lexer = shlex.shlex(stripped, posix=True, punctuation_chars=";&|(){}") + lexer.whitespace_split = True; lexer.commenters = "" + tokens = list(lexer) + except ValueError: + return [] + if any(token in CONTROL_TOKENS or token in CONTROL_WORDS for token in tokens): + return [] + lines = text.splitlines(); commands = []; index = 0 + while index < len(lines): + first = strip_comment(lines[index]).strip() + if not first: + index += 1; continue + parts = [first] + while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): + parts[-1] = parts[-1].rstrip()[:-1]; index += 1 + if continuation := strip_comment(lines[index]).strip(): + parts.append(continuation) + commands.append(" ".join(parts)); index += 1 + return commands + + +def tokens(command: str) -> list[str] | None: + try: + lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") + lexer.whitespace_split = True; lexer.commenters = "" + result = list(lexer) + except ValueError: + return None + if not result or any(token in CONTROL_TOKENS for token in result) or ENV_ASSIGNMENT.fullmatch(result[0]): + return None + return result + + +def normalize(value: str) -> str: + value = value.strip().replace("\\", "/") + while value.startswith("./"): + value = value[2:] + return value.rstrip("/") + + +def ignore_covers(value: str, test_path: str, glob: bool = False) -> bool: + value = normalize(value) + if not value: + return True + test = PurePosixPath(test_path) + if glob: + candidates = [test.as_posix(), test.name] + [p.as_posix() for p in test.parents if p != PurePosixPath(".")] + return any(fnmatch.fnmatchcase(candidate, value) for candidate in candidates) + ignored = PurePosixPath(value) + return ignored.is_absolute() or ".." in ignored.parts or ignored == test or ignored in test.parents + + +def pytest_safe(arguments: list[str], test_path: str, require_path: bool) -> bool: + positive = []; index = 0 + while index < len(arguments): + arg = arguments[index] + if arg in FORBIDDEN_PYTEST_FLAGS or arg.startswith(("-k=", "--keyword=", "-m=", "--markers=", "--deselect=")): + return False + if arg.startswith("--ignore=") and ignore_covers(arg.split("=", 1)[1], test_path): + return False + if arg == "--ignore": + if index + 1 >= len(arguments) or ignore_covers(arguments[index + 1], test_path): + return False + index += 1 + elif arg.startswith("--ignore-glob=") and ignore_covers(arg.split("=", 1)[1], test_path, True): + return False + elif arg == "--ignore-glob": + if index + 1 >= len(arguments) or ignore_covers(arguments[index + 1], test_path, True): + return False + index += 1 + elif arg in SAFE_PYTEST_FLAGS or arg.startswith(SAFE_PYTEST_PREFIXES): + pass + elif arg.startswith("-"): + return False + else: + positive.append(normalize(arg.split("::", 1)[0])) + index += 1 + expected = normalize(test_path) + return expected in positive if require_path else not positive + + +def pytest_command(command: str, test_path: str, require_path: bool) -> bool: + parts = tokens(command) + if not parts: + return False + for start in PYTEST_STARTS: + if tuple(parts[: len(start)]) == start: + return pytest_safe(parts[len(start):], test_path, require_path) + return False + + +def mix_command(command: str, test_path: str, require_path: bool = False) -> bool: + parts = tokens(command) + if not parts or parts[:2] != ["mix", "test"]: + return False + positive = [] + for arg in parts[2:]: + if arg in SAFE_MIX_FLAGS or arg.startswith(SAFE_MIX_PREFIXES): + continue + if arg.startswith("-"): + return False + positive.append(normalize(arg.split(":", 1)[0])) + expected = normalize(test_path) + return (expected in positive) if require_path else (not positive or positive == [expected]) + + +def ci_discovery(discovery: Mapping[str, Any], workflow_texts: str | list[str]) -> tuple[bool, list[str]]: + text = "\n".join(workflow_texts) if isinstance(workflow_texts, list) else workflow_texts + scripts = github_run_scripts(text) + commands = [command for script in scripts for command in shell_commands(script)] + strategy = discovery["strategy"] + uses_pytest = strategy == "pytest_default_discovery" or any(str(p).endswith(".py") for p in discovery.get("contains_any", [])) + if uses_pytest and (any(name.startswith("PYTEST_") for name in yaml_env_names(text)) or any(re.search(r"\bPYTEST_[A-Za-z0-9_]*\b", "\n".join(strip_comment(line) for line in script.splitlines())) for script in scripts)): + return False, [] + if strategy == "pytest_default_discovery": + matched = any(pytest_command(command, str(discovery["test_path"]), False) for command in commands) + return matched, [str(discovery["command"])] if matched else [] + if strategy == "mix_default_discovery": + matched = any(mix_command(command, str(discovery["test_path"])) for command in commands) + return matched, [str(discovery["command"])] if matched else [] + matches = [] + for value in discovery["contains_any"]: + pattern = str(value) + if pattern.endswith(".py") and any(pytest_command(command, pattern, True) for command in commands): + matches.append(pattern) + elif pattern.endswith(".exs") and any(mix_command(command, pattern, True) for command in commands): + matches.append(pattern) + elif not pattern.endswith((".py", ".exs")): + try: + required = shlex.split(pattern) + except ValueError: + continue + if any((parts := tokens(command)) and parts[: len(required)] == required for command in commands): + matches.append(pattern) + return bool(matches), matches diff --git a/standards/lotus-family/conformance/test_causality_model.py b/standards/lotus-family/conformance/test_causality_model.py new file mode 100644 index 00000000..67adc711 --- /dev/null +++ b/standards/lotus-family/conformance/test_causality_model.py @@ -0,0 +1,194 @@ +from __future__ import annotations + +import copy +import json +import tempfile +import unittest +from pathlib import Path + +from lotus_family_auditor import DRIFT, PASS, UNKNOWN, audit_repository, load_manifest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +MANIFEST_PATH = ROOT / "manifest" / "lotus-family-v0.1.json" +GRAPH_PATH = ROOT / "causality" / "lotus-family-causality-v0.1.json" +ROUTES_PATH = ROOT / "causality" / "test-paths-v0.1.json" +TRACEABILITY_PATH = ROOT / "causality" / "TRACEABILITY.md" +SHA = "a" * 40 + + +def _write(root: Path, relative_path: str, content: str) -> None: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _workflow(command: str) -> str: + body = "\n".join(f" {line}" if line else "" for line in command.splitlines()) + return ( + "name: CI\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - name: Run tests\n" + " run: |\n" + f"{body}\n" + ) + + +def _valid_command(discovery: dict) -> str: + strategy = discovery["strategy"] + if strategy == "pytest_default_discovery": + return ( + "python -m pytest \\\n" + " --junitxml=artifacts/junit.xml \\\n" + " --cov=cml \\\n" + " --cov-fail-under=70" + ) + if strategy == "mix_default_discovery": + return "mix test" + pattern = discovery["contains_any"][0] + if pattern.endswith(".py"): + return f"python -m pytest {pattern}" + if pattern.endswith(".exs"): + return f"mix test {pattern}" + return pattern + + +def _materialize(snapshot_root: Path, config: dict, workflow: str | None = None) -> Path: + repository_root = snapshot_root / config["snapshot_dir"] + terms_by_path: dict[str, list[str]] = {} + for check in config["file_checks"]: + terms_by_path.setdefault(check["path"], []).extend(check["contains_all"]) + for relative_path, terms in terms_by_path.items(): + _write(repository_root, relative_path, "\n".join(dict.fromkeys(terms)) + "\n") + discovery = config["ci_discovery"] + _write( + repository_root, + discovery["workflow_paths"][0], + workflow if workflow is not None else _workflow(_valid_command(discovery)), + ) + return repository_root + + +class CausalityModelTest(unittest.TestCase): + def setUp(self) -> None: + self.manifest = load_manifest(MANIFEST_PATH) + self.graph = json.loads(GRAPH_PATH.read_text(encoding="utf-8")) + self.routes = json.loads(ROUTES_PATH.read_text(encoding="utf-8"))["routes"] + + def config(self, repository_id: str) -> dict: + return next( + row + for row in self.manifest["repositories"] + if row["id"] == repository_id + ) + + def test_graph_has_unique_nodes_edges_and_no_dangling_references(self): + node_ids = [node["id"] for node in self.graph["nodes"]] + edge_ids = [edge["id"] for edge in self.graph["edges"]] + self.assertEqual(len(node_ids), len(set(node_ids))) + self.assertEqual(len(edge_ids), len(set(edge_ids))) + known = set(node_ids) + for edge in self.graph["edges"]: + self.assertIn(edge["source"], known) + self.assertIn(edge["target"], known) + + def test_graph_is_acyclic(self): + adjacency: dict[str, list[str]] = {} + for edge in self.graph["edges"]: + adjacency.setdefault(edge["source"], []).append(edge["target"]) + visiting: set[str] = set() + visited: set[str] = set() + + def visit(node: str) -> None: + if node in visiting: + self.fail(f"causality graph contains a cycle at {node}") + if node in visited: + return + visiting.add(node) + for target in adjacency.get(node, []): + visit(target) + visiting.remove(node) + visited.add(node) + + for node in {row["id"] for row in self.graph["nodes"]}: + visit(node) + + def test_every_route_is_a_connected_graph_path(self): + edges = {(edge["source"], edge["target"]) for edge in self.graph["edges"]} + route_ids: set[str] = set() + for route in self.routes: + self.assertNotIn(route["id"], route_ids) + route_ids.add(route["id"]) + path = route["path"] + self.assertGreaterEqual(len(path), 2) + for source, target in zip(path, path[1:]): + self.assertIn((source, target), edges, route["id"]) + + def test_required_graph_nodes_are_covered_by_routes(self): + required_types = set(self.graph["coverage_policy"]["required_node_types"]) + required_nodes = { + node["id"] + for node in self.graph["nodes"] + if node["type"] in required_types + } + covered = {node for route in self.routes for node in route["path"]} + self.assertEqual(required_nodes - covered, set()) + + def test_traceability_names_every_executable_route(self): + traceability = TRACEABILITY_PATH.read_text(encoding="utf-8") + for route in self.routes: + self.assertIn(f"`{route['id']}`", traceability) + + def test_causal_routes_execute_to_their_expected_outcomes(self): + for route in self.routes: + with self.subTest(route=route["id"]): + scenario = route["scenario"] + repository_id = route["repository_id"] + manifest = copy.deepcopy(self.manifest) + commit_sha = scenario.get("commit_sha", SHA) + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + if scenario["kind"] != "missing_snapshot": + config = next( + row + for row in manifest["repositories"] + if row["id"] == repository_id + ) + workflow = scenario.get("workflow") + repository_root = _materialize(snapshot_root, config, workflow) + if scenario["kind"] == "missing_term": + check = config["file_checks"][0] + path = repository_root / check["path"] + path.write_text( + path.read_text(encoding="utf-8").replace( + check["contains_all"][0], "" + ), + encoding="utf-8", + ) + elif scenario["kind"] == "invalid_manifest": + config["file_checks"][0]["contains_all"] = [] + + result = audit_repository( + manifest, + repository_id=repository_id, + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=commit_sha, + ) + self.assertEqual(result["outcome"], route["expected"]["outcome"]) + self.assertEqual( + result["reason_code"], route["expected"]["reason_code"] + ) + identity_mode = scenario.get("assert_identity_mode") + if identity_mode is not None: + self.assertEqual( + result["identity_assurance"]["mode"], identity_mode + ) + self.assertFalse(result["authority"]["grants_merge"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/standards/lotus-family/manifest/lotus-family-v0.1.json b/standards/lotus-family/manifest/lotus-family-v0.1.json index 4cece715..464f8f6f 100644 --- a/standards/lotus-family/manifest/lotus-family-v0.1.json +++ b/standards/lotus-family/manifest/lotus-family-v0.1.json @@ -57,10 +57,9 @@ "workflow_paths": [ ".github/workflows/ci.yml" ], - "strategy": "contains_any", - "contains_any": [ - "mix test" - ] + "strategy": "mix_default_discovery", + "command": "mix test", + "test_path": "test/lotus_docs_contract_test.exs" } }, { From d5d7ea63d7f223f2a96de0b0a0fbc3499b9d9e2c Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:45:20 +0300 Subject: [PATCH 27/79] test: align legacy fixture with Mix discovery --- .../conformance/test_lotus_family_auditor.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/standards/lotus-family/conformance/test_lotus_family_auditor.py b/standards/lotus-family/conformance/test_lotus_family_auditor.py index b32277e5..3a6c3993 100644 --- a/standards/lotus-family/conformance/test_lotus_family_auditor.py +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -12,8 +12,15 @@ def workflow(command, env=""): def fixture(discovery): - if discovery.get("strategy") == "pytest_default_discovery": - command = "python -m pytest \\\n --junitxml=artifacts/junit.xml \\\n --cov=cml\n" + strategy = discovery.get("strategy") + if strategy == "pytest_default_discovery": + command = ( + "python -m pytest \\\n" + " --junitxml=artifacts/junit.xml \\\n" + " --cov=cml\n" + ) + elif strategy == "mix_default_discovery": + command = "mix test\n" else: pattern = discovery["contains_any"][0] command = f"python -m pytest {pattern}\n" if pattern.endswith(".py") else pattern + "\n" @@ -62,7 +69,7 @@ def cml_addopts(self): def ls_addopts(self): self.assert_ci_drift("ls", "PYTEST_ADDOPTS='--ignore=tests/test_lotus_docs_contract.py' " - "python -m pytest tests/test_lotus_docs_contract.py\n") + "python -m pytest tests/test_lotus_docs_contract.py\n") def yaml_addopts(self): From 810d2c89ed17aeba1ad5f4c30b159e130eb00e9d Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:14:09 +0300 Subject: [PATCH 28/79] feat: add causal spacetime trajectory model --- .../lotus-family/causality/SYSTEM-MODEL.md | 37 ++ .../causality/lotus-family-system-v0.1.json | 1 + .../causality/system-routes-v0.1.json | 1 + .../conformance/lotus_family_system_model.py | 151 ++++++ .../conformance/lotus_family_workflow.py | 428 +----------------- .../lotus_family_workflow_hardened.py | 254 +++++++++++ .../lotus_family_workflow_legacy.py | 427 +++++++++++++++++ .../conformance/test_system_model.py | 125 +++++ 8 files changed, 998 insertions(+), 426 deletions(-) create mode 100644 standards/lotus-family/causality/SYSTEM-MODEL.md create mode 100644 standards/lotus-family/causality/lotus-family-system-v0.1.json create mode 100644 standards/lotus-family/causality/system-routes-v0.1.json create mode 100644 standards/lotus-family/conformance/lotus_family_system_model.py create mode 100644 standards/lotus-family/conformance/lotus_family_workflow_hardened.py create mode 100644 standards/lotus-family/conformance/lotus_family_workflow_legacy.py create mode 100644 standards/lotus-family/conformance/test_system_model.py diff --git a/standards/lotus-family/causality/SYSTEM-MODEL.md b/standards/lotus-family/causality/SYSTEM-MODEL.md new file mode 100644 index 00000000..d9ea7478 --- /dev/null +++ b/standards/lotus-family/causality/SYSTEM-MODEL.md @@ -0,0 +1,37 @@ +# Lotus causal spacetime system model + +The system model extends the original causal test graph with four additional views while keeping one audit-only source of truth. + +## Five dimensions + +- **Causal** — why a condition changes risk or outcome. +- **Spatial** — where evidence exists: repository, file, workflow, job, dependency graph, step, shell, test scope, evidence bundle, review, or merge gate. +- **Temporal** — which state must precede another and when evidence becomes stale after the exact head changes. +- **Hierarchy** — how goals, invariants, controls, evidence, outcomes, gates, and authority boundaries relate. +- **Trajectory** — how the system moves toward `PASS`, fail-closed `DRIFT`, evidence staleness, or merge eligibility. + +Machine-readable sources: + +- `lotus-family-system-v0.1.json` — 46 nodes, 62 unique directed relationships, centers, and four canonical trajectories; +- `system-routes-v0.1.json` — 25 connected routes, including runtime regressions and governance-only merge trajectories. + +## Centers without centralized authority + +The model has semantic and evidence centers: current exact head, invariant set, same-byte hashes, and the advisory authority boundary. They centralize identity, meaning, and evidence consistency. They do **not** centralize execution or decision power. + +Centrality scores mean review priority and blast radius only. A highly connected node deserves stronger tests and independent review; it does not gain ownership or approval authority. + +## Time and merge separation + +`PASS` belongs to the audit trajectory. Merge eligibility is a separate trajectory requiring green CI, green security, a fresh exact-head review, and no actionable blockers. Even `state.merge_eligible` terminates at `authority.advisory_only`. + +A head change moves prior evidence to `time.evidence_stale`, which leads to `state.merge_blocked` until checks and review are rerun on the new exact head. + +## New blocker rule + +Every newly discovered blocker must: + +1. add or reuse a system node; +2. add a directed relationship if the causal, spatial, temporal, or hierarchical link is new; +3. include an executable route when runtime behavior is involved; +4. preserve the boundary `PASS != APPROVED != MERGED`. diff --git a/standards/lotus-family/causality/lotus-family-system-v0.1.json b/standards/lotus-family/causality/lotus-family-system-v0.1.json new file mode 100644 index 00000000..258686d7 --- /dev/null +++ b/standards/lotus-family/causality/lotus-family-system-v0.1.json @@ -0,0 +1 @@ +{"schema_version":"pythia.lotus_system_graph.compact.v0.1","model_kind":"causal_spacetime_hierarchy_trajectory","authority":"audit_only","scope":"Lotus Family contract, executable CI discovery, evidence freshness, and merge eligibility separation","dimensions":{"causal":"Why a condition changes risk or outcome","spatial":"Where a condition sits in repository, workflow, job, step, shell, test, evidence, or review topology","temporal":"Which state must precede another and when evidence becomes stale","hierarchy":"How goals, invariants, controls, evidence, outcomes, gates, and authority relate","trajectory":"How the system moves from input to verdict and separately toward merge eligibility"},"temporal_phases":["anchor","input","validation","topology","execution","evidence","verdict","governance","merge"],"node_columns":["id","type","space","time","hierarchy","centrality_role"],"nodes":[["center.exact_head","center","family","anchor","goal","anchor"],["center.invariant_set","center","family","validation","invariant","hub"],["input.snapshot_present","precondition","repository","input","input","ordinary"],["input.snapshot_missing","precondition","repository","input","input","ordinary"],["input.identity_claims_well_formed","claim","repository","input","input","ordinary"],["input.identity_claims_invalid","claim","repository","input","input","ordinary"],["limitation.identity_unverified","limitation","integration","validation","limitation","ordinary"],["control.manifest_valid","control","family","validation","control","ordinary"],["control.manifest_invalid","control","family","validation","control","ordinary"],["control.contract_terms_present","control","file","validation","control","ordinary"],["control.contract_terms_missing","control","file","validation","control","ordinary"],["control.workflow_job_runnable","control","job","topology","control","ordinary"],["control.workflow_job_not_runnable","control","job","topology","control","ordinary"],["control.dependencies_proven","control","job_graph","topology","control","ordinary"],["control.dependencies_unproven","control","job_graph","topology","control","ordinary"],["control.step_enabled","control","step","topology","control","ordinary"],["control.step_skipped","control","step","topology","control","ordinary"],["control.direct_run","control","step","execution","control","ordinary"],["control.non_run_text","control","workflow","execution","control","ordinary"],["control.shell_semantics_known","control","shell","execution","control","ordinary"],["control.shell_semantics_unknown","control","shell","execution","control","ordinary"],["control.test_env_clean","control","environment","execution","control","ordinary"],["control.test_env_override","control","environment","execution","control","ordinary"],["control.shell_straight_line","control","script","execution","control","ordinary"],["control.shell_control_flow","control","script","execution","control","ordinary"],["control.no_terminating_builtin","control","script","execution","control","ordinary"],["control.terminating_builtin","control","script","execution","control","ordinary"],["control.full_test_scope","control","test_scope","execution","control","ordinary"],["control.partial_test_scope","control","test_scope","execution","control","ordinary"],["control.contract_test_selected","control","test_scope","execution","control","ordinary"],["control.contract_test_excluded","control","test_scope","execution","control","ordinary"],["evidence.same_bytes_hashed","evidence","evidence_bundle","evidence","evidence","hub"],["time.evidence_fresh","temporal","evidence_bundle","evidence","evidence","hub"],["time.evidence_stale","temporal","evidence_bundle","evidence","evidence","ordinary"],["risk.false_ci_pass","risk","family","verdict","risk","ordinary"],["risk.identity_overclaim","risk","integration","verdict","risk","ordinary"],["outcome.pass","outcome","audit","verdict","outcome","hub"],["outcome.drift","outcome","audit","verdict","outcome","ordinary"],["outcome.unknown","outcome","audit","verdict","outcome","ordinary"],["gate.ci_green","gate","github","evidence","gate","ordinary"],["gate.security_green","gate","github","evidence","gate","ordinary"],["gate.review_fresh","gate","review","governance","gate","ordinary"],["gate.no_actionable_blockers","gate","review","governance","gate","ordinary"],["state.merge_eligible","state","merge","merge","state","ordinary"],["state.merge_blocked","state","merge","merge","state","ordinary"],["authority.advisory_only","boundary","family","governance","authority","boundary"]],"edge_columns":["id","source","target","relation","dimension"],"edges":[["E01","center.exact_head","input.snapshot_present","anchors","temporal"],["E02","center.exact_head","input.snapshot_missing","anchors","temporal"],["E03","input.snapshot_present","input.identity_claims_well_formed","enables","causal"],["E04","input.snapshot_present","input.identity_claims_invalid","may_reveal","causal"],["E05","input.snapshot_missing","outcome.unknown","causes","causal"],["E06","input.identity_claims_invalid","outcome.unknown","causes","causal"],["E07","input.identity_claims_well_formed","limitation.identity_unverified","retains_limitation","causal"],["E08","limitation.identity_unverified","center.invariant_set","permits_bounded_audit","hierarchy"],["E09","limitation.identity_unverified","risk.identity_overclaim","creates_if_misrepresented","causal"],["E10","risk.identity_overclaim","authority.advisory_only","is_bounded_by","authority"],["E11","center.invariant_set","control.manifest_valid","defines","hierarchy"],["E12","center.invariant_set","control.manifest_invalid","may_reveal","hierarchy"],["E13","control.manifest_invalid","outcome.unknown","causes","causal"],["E14","control.manifest_valid","control.contract_terms_present","enables_check","causal"],["E15","control.manifest_valid","control.contract_terms_missing","may_reveal","causal"],["E16","control.contract_terms_missing","outcome.drift","causes","causal"],["E17","control.contract_terms_present","control.workflow_job_runnable","enables_ci_topology","spatial"],["E18","control.contract_terms_present","control.workflow_job_not_runnable","may_reveal","spatial"],["E19","control.workflow_job_not_runnable","control.non_run_text","may_reveal","spatial"],["E20","control.non_run_text","risk.false_ci_pass","would_create_without_guard","causal"],["E21","control.workflow_job_not_runnable","risk.false_ci_pass","would_create_without_guard","causal"],["E22","control.workflow_job_runnable","control.dependencies_proven","requires","temporal"],["E23","control.workflow_job_runnable","control.dependencies_unproven","may_reveal","temporal"],["E24","control.dependencies_unproven","risk.false_ci_pass","would_create_without_guard","causal"],["E25","control.dependencies_proven","control.step_enabled","enables","spatial"],["E26","control.dependencies_proven","control.step_skipped","may_reveal","spatial"],["E27","control.step_skipped","risk.false_ci_pass","would_create_without_guard","causal"],["E28","control.step_enabled","control.direct_run","enables","spatial"],["E29","control.step_enabled","control.non_run_text","may_reveal","spatial"],["E31","control.direct_run","control.shell_semantics_known","requires","spatial"],["E32","control.direct_run","control.shell_semantics_unknown","may_reveal","spatial"],["E33","control.shell_semantics_unknown","risk.false_ci_pass","would_create_without_guard","causal"],["E34","control.shell_semantics_known","control.test_env_clean","requires","causal"],["E35","control.shell_semantics_known","control.test_env_override","may_reveal","causal"],["E36","control.test_env_override","risk.false_ci_pass","would_create_without_guard","causal"],["E37","control.test_env_clean","control.shell_straight_line","requires","causal"],["E38","control.test_env_clean","control.shell_control_flow","may_reveal","causal"],["E39","control.shell_control_flow","risk.false_ci_pass","would_create_without_guard","causal"],["E40","control.shell_straight_line","control.no_terminating_builtin","requires","temporal"],["E41","control.shell_straight_line","control.terminating_builtin","may_reveal","temporal"],["E42","control.terminating_builtin","risk.false_ci_pass","would_create_without_guard","causal"],["E43","control.no_terminating_builtin","control.full_test_scope","enables","causal"],["E44","control.no_terminating_builtin","control.partial_test_scope","may_reveal","causal"],["E45","control.partial_test_scope","control.contract_test_excluded","causes","causal"],["E46","control.contract_test_excluded","risk.false_ci_pass","would_create_without_guard","causal"],["E47","control.full_test_scope","control.contract_test_selected","proves","causal"],["E48","risk.false_ci_pass","outcome.drift","is_prevented_by_fail_closed_result","causal"],["E49","control.contract_test_selected","evidence.same_bytes_hashed","enables_evidence","causal"],["E50","evidence.same_bytes_hashed","time.evidence_fresh","binds_to_current_head","temporal"],["E51","evidence.same_bytes_hashed","time.evidence_stale","may_be_superseded","temporal"],["E52","time.evidence_fresh","outcome.pass","supports","causal"],["E53","outcome.pass","authority.advisory_only","remains_bounded_by","authority"],["E54","time.evidence_stale","state.merge_blocked","blocks","temporal"],["E55","center.exact_head","gate.ci_green","anchors","temporal"],["E56","gate.ci_green","gate.security_green","coexists_with","temporal"],["E57","gate.security_green","outcome.pass","combines_with","temporal"],["E58","outcome.pass","gate.review_fresh","requires_for_merge","temporal"],["E59","gate.review_fresh","gate.no_actionable_blockers","enables","governance"],["E60","gate.no_actionable_blockers","state.merge_eligible","enables","governance"],["E61","state.merge_eligible","authority.advisory_only","does_not_grant_authority","authority"],["E62","outcome.drift","state.merge_blocked","blocks","governance"],["E63","outcome.unknown","state.merge_blocked","blocks","governance"]],"centers":[["center.exact_head","evidence_anchor","identity and freshness, not authority"],["center.invariant_set","semantic_anchor","meaning and coverage"],["evidence.same_bytes_hashed","evidence_hub","evaluated bytes and hashes"],["authority.advisory_only","authority_boundary","limitations without granting execution"]],"trajectories":[["audit-pass",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"Bounded snapshot audit reaches PASS without granting authority"],["false-pass-prevention",["control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift","state.merge_blocked"],"Ambiguous execution semantics fail closed"],["evidence-staleness",["evidence.same_bytes_hashed","time.evidence_stale","state.merge_blocked"],"Head changes invalidate downstream merge evidence"],["merge-eligibility",["center.exact_head","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"Independent gates converge on eligibility while authority remains human"]],"centrality_policy":{"meaning":"Centrality identifies review priority and blast radius, never ownership or authority.","critical_nodes":["center.exact_head","center.invariant_set","control.direct_run","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"minimum_route_coverage":2},"coverage_policy":{"required_node_types":["center","control","evidence","temporal","outcome","boundary"],"route_file":"system-routes-v0.1.json","new_blocker_rule":"Every new blocker must add or reuse a graph node and at least one executable route.","derived_views":["causal","spatial","temporal","hierarchy","trajectory","traceability"]}} diff --git a/standards/lotus-family/causality/system-routes-v0.1.json b/standards/lotus-family/causality/system-routes-v0.1.json new file mode 100644 index 00000000..c35307c0 --- /dev/null +++ b/standards/lotus-family/causality/system-routes-v0.1.json @@ -0,0 +1 @@ +{"schema_version":"pythia.lotus_system_routes.compact.v0.1","graph":"lotus-family-system-v0.1.json","route_columns":["id","repo","scenario","path","trajectory","outcome","reason"],"routes":[["CML-PASS-001","cml","valid",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["LS-PASS-001","ls","valid",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-PASS-001","pythia","valid",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-NONRUN-001","cml","ci-nonrun-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","control.non_run_text","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SKIP-001","cml","ci-step-skip-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_skipped","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-SKIP-001","cml","ci-job-skip-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NO-RUNNER-001","cml","ci-no-runner-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-QUOTED-ENV-001","cml","ci-quoted-env-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_override","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-CONTROL-001","cml","ci-shell-control-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_control_flow","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CML-SUBSET-001","cml","cml-subset-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["PYTHIA-SUBSET-001","pythia","pythia-subset-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-FAKE-STEPS-001","cml","ci-fake-steps-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-CUSTOM-SHELL-001","cml","ci-custom-shell-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-TERMINATOR-EXIT-001","cml","ci-terminator-exit-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-SKIP-001","cml","ci-needs-skip-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-ALWAYS-PASS-001","cml","ci-needs-always-pass-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-LINE-SUBSET-001","pythia","pythia-line-subset-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["LS-NODE-SUBSET-001","ls","ls-node-subset-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["IDENTITY-INVALID-001","cml","invalid_commit",["center.exact_head","input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","COMMIT_SHA_INVALID"],["SNAPSHOT-MISSING-001","cml","missing_snapshot",["center.exact_head","input.snapshot_missing","outcome.unknown"],"audit","UNKNOWN","SNAPSHOT_UNAVAILABLE"],["CONTRACT-DRIFT-001","cml","missing_term",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_missing","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["IDENTITY-LIMIT-001","cml","valid_identity",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","risk.identity_overclaim","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["MANIFEST-INVALID-001","cml","invalid_manifest",["center.invariant_set","control.manifest_invalid","outcome.unknown"],"audit","UNKNOWN","MANIFEST_INVALID"],["MERGE-ELIGIBLE-001","cml","valid",["center.exact_head","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"merge","MODEL","MODEL_ONLY"],["MERGE-STALE-001","cml","valid",["evidence.same_bytes_hashed","time.evidence_stale","state.merge_blocked"],"merge","MODEL","MODEL_ONLY"]]} diff --git a/standards/lotus-family/conformance/lotus_family_system_model.py b/standards/lotus-family/conformance/lotus_family_system_model.py new file mode 100644 index 00000000..9504097a --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_system_model.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import json +from collections import Counter +from pathlib import Path +from typing import Any + +NODE_COLS = ["id", "type", "space", "time", "hierarchy", "centrality_role"] +EDGE_COLS = ["id", "source", "target", "relation", "dimension"] +ROUTE_COLS = ["id", "repo", "scenario", "path", "trajectory", "outcome", "reason"] + + +def load(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError("model root must be an object") + return value + + +def rows(model: dict[str, Any], key: str, columns: list[str]) -> list[dict[str, Any]]: + declared = model.get(f"{key[:-1]}_columns") if key.endswith("s") else None + if declared != columns: + raise ValueError(f"unexpected {key} columns") + result = [] + for raw in model.get(key, []): + if not isinstance(raw, list) or len(raw) != len(columns): + raise ValueError(f"invalid {key} row") + result.append(dict(zip(columns, raw))) + return result + + +def validate_graph(graph: dict[str, Any]) -> dict[str, Any]: + if graph.get("schema_version") != "pythia.lotus_system_graph.compact.v0.1": + raise ValueError("unsupported system graph schema") + if graph.get("authority") != "audit_only": + raise ValueError("system graph must remain audit_only") + required_dimensions = {"causal", "spatial", "temporal", "hierarchy", "trajectory"} + if set(graph.get("dimensions", [])) != required_dimensions: + raise ValueError("all five graph dimensions are required") + nodes = rows(graph, "nodes", NODE_COLS) + edges = rows(graph, "edges", EDGE_COLS) + node_ids = [node["id"] for node in nodes] + edge_ids = [edge["id"] for edge in edges] + if len(node_ids) != len(set(node_ids)) or len(edge_ids) != len(set(edge_ids)): + raise ValueError("node and edge ids must be unique") + known = set(node_ids) + phases = graph.get("temporal_phases", []) + phase_index = {phase: index for index, phase in enumerate(phases)} + for node in nodes: + if not all(node[field] for field in ("space", "time", "hierarchy")): + raise ValueError(f"node lacks spacetime hierarchy: {node['id']}") + if node["time"] not in phase_index and node["time"] != "anchor": + raise ValueError(f"unknown temporal phase: {node['id']}") + adjacency: dict[str, list[str]] = {} + edge_pairs = set() + for edge in edges: + if edge["source"] not in known or edge["target"] not in known: + raise ValueError(f"dangling edge: {edge['id']}") + pair = (edge["source"], edge["target"]) + if pair in edge_pairs: + raise ValueError(f"duplicate edge pair: {pair}") + edge_pairs.add(pair) + adjacency.setdefault(edge["source"], []).append(edge["target"]) + visiting: set[str] = set() + visited: set[str] = set() + + def visit(node: str) -> None: + if node in visiting: + raise ValueError(f"cycle at {node}") + if node in visited: + return + visiting.add(node) + for target in adjacency.get(node, []): + visit(target) + visiting.remove(node) + visited.add(node) + + for node in known: + visit(node) + centers = graph.get("centers", []) + center_ids = {row[0] for row in centers} + if not center_ids or not center_ids <= known: + raise ValueError("centers must reference graph nodes") + trajectories = graph.get("trajectories", []) + for trajectory_id, path, _ in trajectories: + if len(path) < 2 or any(node not in known for node in path): + raise ValueError(f"invalid trajectory: {trajectory_id}") + for pair in zip(path, path[1:]): + if pair not in edge_pairs: + raise ValueError(f"trajectory gap {trajectory_id}: {pair}") + return {"nodes": nodes, "edges": edges, "edge_pairs": edge_pairs, "centers": center_ids} + + +def validate_routes(graph: dict[str, Any], route_model: dict[str, Any]) -> list[dict[str, Any]]: + if route_model.get("schema_version") != "pythia.lotus_system_routes.compact.v0.1": + raise ValueError("unsupported route schema") + view = validate_graph(graph) + routes = rows(route_model, "routes", ROUTE_COLS) + route_ids: set[str] = set() + for route in routes: + if route["id"] in route_ids: + raise ValueError(f"duplicate route: {route['id']}") + route_ids.add(route["id"]) + path = route["path"] + if len(path) < 2: + raise ValueError(f"route too short: {route['id']}") + for pair in zip(path, path[1:]): + if pair not in view["edge_pairs"]: + raise ValueError(f"route gap {route['id']}: {pair}") + return routes + + +def centrality_report(graph: dict[str, Any], routes: list[dict[str, Any]]) -> dict[str, Any]: + view = validate_graph(graph) + degree = Counter() + for edge in view["edges"]: + degree[edge["source"]] += 1 + degree[edge["target"]] += 1 + route_hits = Counter(node for route in routes for node in route["path"]) + maximum = max(route_hits.values(), default=1) + nodes = [] + for node in view["nodes"]: + node_id = node["id"] + nodes.append({ + "id": node_id, + "degree": degree[node_id], + "route_hits": route_hits[node_id], + "blast_radius": round(route_hits[node_id] / maximum, 3), + "is_center": node_id in view["centers"], + }) + nodes.sort(key=lambda row: (-row["route_hits"], -row["degree"], row["id"])) + return { + "schema_version": "pythia.lotus_centrality_report.v0.1", + "meaning": "review priority and blast radius only; never ownership or authority", + "nodes": nodes, + } + + +def traceability(routes: list[dict[str, Any]]) -> str: + lines = [ + "# Lotus system route ledger", + "", + "| Route | Repository | Scenario | Trajectory | Expected |", + "|---|---|---|---|---|", + ] + for route in routes: + lines.append( + f"| `{route['id']}` | `{route['repo']}` | `{route['scenario']}` | " + f"`{route['trajectory'] or 'runtime'}` | `{route['outcome']} / {route['reason']}` |" + ) + return "\n".join(lines) + "\n" diff --git a/standards/lotus-family/conformance/lotus_family_workflow.py b/standards/lotus-family/conformance/lotus_family_workflow.py index 29a79bf8..a650703e 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow.py +++ b/standards/lotus-family/conformance/lotus_family_workflow.py @@ -1,427 +1,3 @@ -from __future__ import annotations +"""Public fail-closed GitHub Actions discovery API.""" -import fnmatch -import json -import re -import shlex -from pathlib import PurePosixPath -from typing import Any, Mapping - -BARE_KEY = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*\Z") -BLOCK_SCALAR = re.compile(r"([|>])(?:[1-9][+-]?|[+-][1-9]?)?\Z") -ENV_ASSIGNMENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*=.*\Z") -PYTEST_STARTS = (("python", "-m", "pytest"), ("pytest",)) -SAFE_PYTEST_FLAGS = {"-q", "--quiet", "-v", "--verbose", "--strict-markers", "--strict-config", "--disable-warnings"} -SAFE_PYTEST_PREFIXES = ("--junitxml=", "--cov=", "--cov-report=", "--cov-fail-under=", "--color=", "--tb=", "--durations=", "--maxfail=") -FORBIDDEN_PYTEST_FLAGS = {"--collect-only", "--co", "--setup-only", "--pyargs", "-k", "--keyword", "-m", "--markers", "--deselect"} -SAFE_MIX_FLAGS = {"--trace", "--color"} -SAFE_MIX_PREFIXES = ("--seed=", "--max-failures=") -CONTROL_TOKENS = {"&&", "||", ";", "|", "&", "(", ")", "{", "}"} -CONTROL_WORDS = {"if", "then", "elif", "else", "fi", "for", "while", "until", "do", "done", "case", "esac", "select", "function"} - - -def strip_comment(line: str) -> str: - single = double = escaped = False - for index, char in enumerate(line): - if escaped: - escaped = False - elif char == "\\" and not single: - escaped = True - elif char == "'" and not double: - single = not single - elif char == '"' and not single: - double = not double - elif char == "#" and not single and not double: - return line[:index] - return line - - -def indent_of(line: str) -> int | None: - prefix = line[: len(line) - len(line.lstrip(" \t"))] - return None if "\t" in prefix else len(prefix) - - -def decode_key(text: str) -> str | None: - key = text.strip() - if BARE_KEY.fullmatch(key): - return key - if len(key) >= 2 and key[0] == key[-1] == "'": - return key[1:-1].replace("''", "'") or None - if len(key) >= 2 and key[0] == key[-1] == '"': - try: - decoded = json.loads(key) - except json.JSONDecodeError: - return None - return decoded if isinstance(decoded, str) and decoded else None - return None - - -def split_pair(text: str) -> tuple[str, str] | None: - single = double = escaped = False - for index, char in enumerate(text): - if escaped: - escaped = False - elif char == "\\" and double: - escaped = True - elif char == "'" and not double: - single = not single - elif char == '"' and not single: - double = not double - elif char == ":" and not single and not double: - return text[:index], text[index + 1 :] - return None - - -def yaml_header(line: str) -> tuple[int, bool, int, str, str] | None: - indent = indent_of(line) - if indent is None: - return None - text = line[indent:] - if not text or text.startswith("#"): - return None - is_list = False - key_indent = indent - if text.startswith("-"): - match = re.match(r"-([ ]+)(.*)\Z", text) - if not match: - return None - is_list = True - key_indent = indent + 1 + len(match.group(1)) - text = match.group(2) - pair = split_pair(text) - if not pair: - return None - key = decode_key(pair[0]) - return None if key is None else (indent, is_list, key_indent, key, pair[1]) - - -def scalar_indicator(value: str) -> str | None: - match = BLOCK_SCALAR.fullmatch(strip_comment(value).strip()) - return match.group(1) if match else None - - -def inline_scalar(value: str) -> str | None: - text = strip_comment(value).strip() - if not text or text in {"null", "Null", "NULL", "~"}: - return None - if text.startswith("'"): - return text[1:-1].replace("''", "'") if text.endswith("'") else None - if text.startswith('"'): - if not text.endswith('"'): - return None - try: - value = json.loads(text) - except json.JSONDecodeError: - return None - return value if isinstance(value, str) else None - return text - - -def condition_true(value: str) -> bool: - scalar = inline_scalar(value) - return scalar is not None and re.sub(r"\s+", " ", scalar.strip()).lower() in {"true", "${{ true }}"} - - -def scalar_ranges(lines: list[str]) -> dict[int, tuple[int, str]]: - result: dict[int, tuple[int, str]] = {} - index = 0 - while index < len(lines): - header = yaml_header(lines[index]) - style = scalar_indicator(header[4]) if header else None - if not header or not style: - index += 1 - continue - end = index + 1 - while end < len(lines): - if not lines[end].strip(): - end += 1 - continue - indent = indent_of(lines[end]) - if indent is None or indent <= header[2]: - break - end += 1 - result[index] = (end, style) - index = end - return result - - -def scalar_text(lines: list[str], start: int, end: int, style: str) -> str: - body = lines[start + 1 : end] - indents = [indent for line in body if line.strip() and (indent := indent_of(line)) is not None] - if not indents: - return "" - minimum = min(indents) - body = [line[minimum:] if line.strip() else "" for line in body] - if style == "|": - return "\n".join(body) + "\n" - paragraphs: list[str] = [] - current: list[str] = [] - for line in body: - if line: - current.append(line) - elif current: - paragraphs.append(" ".join(current)); current = [] - elif paragraphs: - paragraphs.append("") - if current: - paragraphs.append(" ".join(current)) - return "\n".join(paragraphs) + ("\n" if paragraphs else "") - - -def block_end(lines: list[str], start: int, parent_indent: int) -> int: - index = start + 1 - while index < len(lines): - if not lines[index].strip() or lines[index].lstrip().startswith("#"): - index += 1; continue - indent = indent_of(lines[index]) - if indent is None or indent <= parent_indent: - break - index += 1 - return index - - -def direct_headers(lines: list[str], start: int, end: int, parent_indent: int, scalar_body: set[int]): - found = [] - minimum = None - for row in range(start, end): - if row in scalar_body: - continue - header = yaml_header(lines[row]) - if not header or header[1] or header[2] <= parent_indent: - continue - if minimum is None or header[2] < minimum: - minimum, found = header[2], [(row, header)] - elif header[2] == minimum: - found.append((row, header)) - return found - - -def item_starts(lines: list[str], start: int, end: int, scalar_body: set[int]) -> list[int]: - items: list[int] = [] - minimum = None - for row in range(start, end): - if row in scalar_body: - continue - indent = indent_of(lines[row]) - text = lines[row][indent:] if indent is not None else "" - if indent is not None and text.startswith("-"): - minimum = indent if minimum is None else minimum - if indent == minimum: - items.append(row) - return items - - -def github_run_scripts(text: str) -> list[str]: - lines = text.splitlines() - ranges = scalar_ranges(lines) - scalar_body = {row for start, (end, _) in ranges.items() for row in range(start + 1, end)} - scripts: list[str] = [] - jobs = [(row, h) for row, line in enumerate(lines) if row not in scalar_body and (h := yaml_header(line)) and not h[1] and h[2] == 0 and h[3] == "jobs" and inline_scalar(h[4]) is None and scalar_indicator(h[4]) is None] - for jobs_row, jobs_header in jobs: - jobs_end = block_end(lines, jobs_row, jobs_header[2]) - job_headers = direct_headers(lines, jobs_row + 1, jobs_end, jobs_header[2], scalar_body) - for ji, (job_row, job_header) in enumerate(job_headers): - job_end = job_headers[ji + 1][0] if ji + 1 < len(job_headers) else jobs_end - props = {h[3]: (row, h) for row, h in direct_headers(lines, job_row + 1, job_end, job_header[2], scalar_body)} - runs_on = props.get("runs-on") - if not runs_on or inline_scalar(runs_on[1][4]) is None: - continue - job_if = props.get("if") - if job_if and not condition_true(job_if[1][4]): - continue - steps = props.get("steps") - if not steps or inline_scalar(steps[1][4]) is not None or scalar_indicator(steps[1][4]) is not None: - continue - steps_end = block_end(lines, steps[0], steps[1][2]) - items = item_starts(lines, steps[0] + 1, steps_end, scalar_body) - for si, item_row in enumerate(items): - item_end = items[si + 1] if si + 1 < len(items) else steps_end - first = yaml_header(lines[item_row]) - if not first or not first[1]: - continue - props = {first[3]: (item_row, first)} - for row in range(item_row + 1, item_end): - if row in scalar_body: - continue - header = yaml_header(lines[row]) - if header and not header[1] and header[2] == first[2]: - props[header[3]] = (row, header) - step_if = props.get("if") - if step_if and not condition_true(step_if[1][4]): - continue - run = props.get("run") - if not run: - continue - if run[0] in ranges: - end, style = ranges[run[0]] - scripts.append(scalar_text(lines, run[0], end, style)) - elif (value := inline_scalar(run[1][4])) is not None: - scripts.append(value) - return scripts - - -def yaml_env_names(text: str) -> set[str]: - lines = text.splitlines(); ranges = scalar_ranges(lines) - body = {row for start, (end, _) in ranges.items() for row in range(start + 1, end)} - names: set[str] = set() - for index, line in enumerate(lines): - if index in body: - continue - header = yaml_header(line) - if not header or header[3] != "env": - continue - inline = strip_comment(header[4]).strip() - if inline: - names.update(re.findall(r"\bPYTEST_[A-Za-z0-9_]*\b", inline)); continue - for row in range(index + 1, len(lines)): - if row in body: - continue - if not lines[row].strip() or lines[row].lstrip().startswith("#"): - continue - indent = indent_of(lines[row]) - if indent is None or indent <= header[2]: - break - child = yaml_header(lines[row]) - if child and not child[1] and child[2] > header[2]: - names.add(child[3]) - return names - - -def shell_commands(text: str) -> list[str]: - stripped = "\n".join(strip_comment(line) for line in text.splitlines()) - try: - lexer = shlex.shlex(stripped, posix=True, punctuation_chars=";&|(){}") - lexer.whitespace_split = True; lexer.commenters = "" - tokens = list(lexer) - except ValueError: - return [] - if any(token in CONTROL_TOKENS or token in CONTROL_WORDS for token in tokens): - return [] - lines = text.splitlines(); commands = []; index = 0 - while index < len(lines): - first = strip_comment(lines[index]).strip() - if not first: - index += 1; continue - parts = [first] - while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): - parts[-1] = parts[-1].rstrip()[:-1]; index += 1 - if continuation := strip_comment(lines[index]).strip(): - parts.append(continuation) - commands.append(" ".join(parts)); index += 1 - return commands - - -def tokens(command: str) -> list[str] | None: - try: - lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") - lexer.whitespace_split = True; lexer.commenters = "" - result = list(lexer) - except ValueError: - return None - if not result or any(token in CONTROL_TOKENS for token in result) or ENV_ASSIGNMENT.fullmatch(result[0]): - return None - return result - - -def normalize(value: str) -> str: - value = value.strip().replace("\\", "/") - while value.startswith("./"): - value = value[2:] - return value.rstrip("/") - - -def ignore_covers(value: str, test_path: str, glob: bool = False) -> bool: - value = normalize(value) - if not value: - return True - test = PurePosixPath(test_path) - if glob: - candidates = [test.as_posix(), test.name] + [p.as_posix() for p in test.parents if p != PurePosixPath(".")] - return any(fnmatch.fnmatchcase(candidate, value) for candidate in candidates) - ignored = PurePosixPath(value) - return ignored.is_absolute() or ".." in ignored.parts or ignored == test or ignored in test.parents - - -def pytest_safe(arguments: list[str], test_path: str, require_path: bool) -> bool: - positive = []; index = 0 - while index < len(arguments): - arg = arguments[index] - if arg in FORBIDDEN_PYTEST_FLAGS or arg.startswith(("-k=", "--keyword=", "-m=", "--markers=", "--deselect=")): - return False - if arg.startswith("--ignore=") and ignore_covers(arg.split("=", 1)[1], test_path): - return False - if arg == "--ignore": - if index + 1 >= len(arguments) or ignore_covers(arguments[index + 1], test_path): - return False - index += 1 - elif arg.startswith("--ignore-glob=") and ignore_covers(arg.split("=", 1)[1], test_path, True): - return False - elif arg == "--ignore-glob": - if index + 1 >= len(arguments) or ignore_covers(arguments[index + 1], test_path, True): - return False - index += 1 - elif arg in SAFE_PYTEST_FLAGS or arg.startswith(SAFE_PYTEST_PREFIXES): - pass - elif arg.startswith("-"): - return False - else: - positive.append(normalize(arg.split("::", 1)[0])) - index += 1 - expected = normalize(test_path) - return expected in positive if require_path else not positive - - -def pytest_command(command: str, test_path: str, require_path: bool) -> bool: - parts = tokens(command) - if not parts: - return False - for start in PYTEST_STARTS: - if tuple(parts[: len(start)]) == start: - return pytest_safe(parts[len(start):], test_path, require_path) - return False - - -def mix_command(command: str, test_path: str, require_path: bool = False) -> bool: - parts = tokens(command) - if not parts or parts[:2] != ["mix", "test"]: - return False - positive = [] - for arg in parts[2:]: - if arg in SAFE_MIX_FLAGS or arg.startswith(SAFE_MIX_PREFIXES): - continue - if arg.startswith("-"): - return False - positive.append(normalize(arg.split(":", 1)[0])) - expected = normalize(test_path) - return (expected in positive) if require_path else (not positive or positive == [expected]) - - -def ci_discovery(discovery: Mapping[str, Any], workflow_texts: str | list[str]) -> tuple[bool, list[str]]: - text = "\n".join(workflow_texts) if isinstance(workflow_texts, list) else workflow_texts - scripts = github_run_scripts(text) - commands = [command for script in scripts for command in shell_commands(script)] - strategy = discovery["strategy"] - uses_pytest = strategy == "pytest_default_discovery" or any(str(p).endswith(".py") for p in discovery.get("contains_any", [])) - if uses_pytest and (any(name.startswith("PYTEST_") for name in yaml_env_names(text)) or any(re.search(r"\bPYTEST_[A-Za-z0-9_]*\b", "\n".join(strip_comment(line) for line in script.splitlines())) for script in scripts)): - return False, [] - if strategy == "pytest_default_discovery": - matched = any(pytest_command(command, str(discovery["test_path"]), False) for command in commands) - return matched, [str(discovery["command"])] if matched else [] - if strategy == "mix_default_discovery": - matched = any(mix_command(command, str(discovery["test_path"])) for command in commands) - return matched, [str(discovery["command"])] if matched else [] - matches = [] - for value in discovery["contains_any"]: - pattern = str(value) - if pattern.endswith(".py") and any(pytest_command(command, pattern, True) for command in commands): - matches.append(pattern) - elif pattern.endswith(".exs") and any(mix_command(command, pattern, True) for command in commands): - matches.append(pattern) - elif not pattern.endswith((".py", ".exs")): - try: - required = shlex.split(pattern) - except ValueError: - continue - if any((parts := tokens(command)) and parts[: len(required)] == required for command in commands): - matches.append(pattern) - return bool(matches), matches +from lotus_family_workflow_hardened import * # noqa: F403 diff --git a/standards/lotus-family/conformance/lotus_family_workflow_hardened.py b/standards/lotus-family/conformance/lotus_family_workflow_hardened.py new file mode 100644 index 00000000..0cb76f8f --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_hardened.py @@ -0,0 +1,254 @@ +from __future__ import annotations + +import re +import shlex +from pathlib import PurePosixPath +from typing import Any, Mapping + +import lotus_family_workflow_legacy as legacy + +_CONTROL = legacy.CONTROL_TOKENS | {"(", ")", "{", "}"} +_CONTROL_WORDS = legacy.CONTROL_WORDS +_TERMINATORS = {"exit", "exec", "return", "break", "continue"} +_POSIX_SHELLS = {"bash", "sh"} + + +def _condition(value: str) -> str | None: + scalar = legacy.inline_scalar(value) + if scalar is None: + return None + text = re.sub(r"\s+", " ", scalar.strip()).lower() + if text.startswith("${{") and text.endswith("}}"): + text = text[3:-2].strip() + return text + + +def _supported_shell(value: str) -> bool: + scalar = legacy.inline_scalar(value) + if scalar is None: + return False + try: + parts = shlex.split(scalar) + except ValueError: + return False + if not parts or PurePosixPath(parts[0]).name not in _POSIX_SHELLS: + return False + return len(parts) == 1 or (parts[-1] == "{0}" and all(p.startswith("-") for p in parts[1:-1])) + + +def _parse_needs(value: str) -> list[str] | None: + scalar = legacy.inline_scalar(value) + if scalar is None: + return None + text = scalar.strip() + if text.startswith("[") and text.endswith("]"): + body = text[1:-1].strip() + if not body: + return [] + values = [legacy.decode_key(item.strip()) for item in body.split(",")] + return None if any(value is None for value in values) else [str(value) for value in values] + key = legacy.decode_key(text) + return [key] if key else None + + +def github_run_scripts(text: str) -> list[str]: + lines = text.splitlines() + ranges = legacy.scalar_ranges(lines) + scalar_body = { + row for start, (end, _) in ranges.items() for row in range(start + 1, end) + } + scripts: list[str] = [] + jobs = [ + (row, header) + for row, line in enumerate(lines) + if row not in scalar_body + and (header := legacy.yaml_header(line)) + and not header[1] + and header[2] == 0 + and header[3] == "jobs" + and legacy.inline_scalar(header[4]) is None + and legacy.scalar_indicator(header[4]) is None + ] + for jobs_row, jobs_header in jobs: + jobs_end = legacy.block_end(lines, jobs_row, jobs_header[2]) + job_headers = legacy.direct_headers(lines, jobs_row + 1, jobs_end, jobs_header[2], scalar_body) + job_names = {header[3] for _, header in job_headers} + for index, (job_row, job_header) in enumerate(job_headers): + job_end = job_headers[index + 1][0] if index + 1 < len(job_headers) else jobs_end + props = { + header[3]: (row, header) + for row, header in legacy.direct_headers(lines, job_row + 1, job_end, job_header[2], scalar_body) + } + runs_on = props.get("runs-on") + if not runs_on or legacy.inline_scalar(runs_on[1][4]) is None: + continue + needs = props.get("needs") + if needs: + dependencies = _parse_needs(needs[1][4]) + if dependencies is None or not dependencies or any(dep not in job_names for dep in dependencies): + continue + job_if = props.get("if") + if not job_if or _condition(job_if[1][4]) != "always()": + continue + else: + job_if = props.get("if") + if job_if and _condition(job_if[1][4]) != "true": + continue + steps = props.get("steps") + if not steps or legacy.inline_scalar(steps[1][4]) is not None or legacy.scalar_indicator(steps[1][4]) is not None: + continue + steps_end = legacy.block_end(lines, steps[0], steps[1][2]) + items = legacy.item_starts(lines, steps[0] + 1, steps_end, scalar_body) + for step_index, item_row in enumerate(items): + item_end = items[step_index + 1] if step_index + 1 < len(items) else steps_end + first = legacy.yaml_header(lines[item_row]) + if not first or not first[1]: + continue + step_props = {first[3]: (item_row, first)} + for row in range(item_row + 1, item_end): + if row in scalar_body: + continue + header = legacy.yaml_header(lines[row]) + if header and not header[1] and header[2] == first[2]: + step_props[header[3]] = (row, header) + step_if = step_props.get("if") + if step_if and _condition(step_if[1][4]) != "true": + continue + shell = step_props.get("shell") + if shell and not _supported_shell(shell[1][4]): + continue + run = step_props.get("run") + if not run: + continue + if run[0] in ranges: + end, style = ranges[run[0]] + scripts.append(legacy.scalar_text(lines, run[0], end, style)) + elif (value := legacy.inline_scalar(run[1][4])) is not None: + scripts.append(value) + return scripts + + +def shell_commands(text: str) -> list[str]: + stripped = "\n".join(legacy.strip_comment(line) for line in text.splitlines()) + try: + lexer = shlex.shlex(stripped, posix=True, punctuation_chars=";&|(){}") + lexer.whitespace_split = True + lexer.commenters = "" + script_tokens = list(lexer) + except ValueError: + return [] + if any(token in _CONTROL or token in _CONTROL_WORDS for token in script_tokens): + return [] + commands: list[str] = [] + lines = text.splitlines() + index = 0 + while index < len(lines): + first = legacy.strip_comment(lines[index]).strip() + if not first: + index += 1 + continue + parts = [first] + while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): + parts[-1] = parts[-1].rstrip()[:-1] + index += 1 + continuation = legacy.strip_comment(lines[index]).strip() + if continuation: + parts.append(continuation) + command = " ".join(parts) + try: + parsed = shlex.split(command) + except ValueError: + return [] + if parsed and (legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) or parsed[0] in _TERMINATORS): + return [] + commands.append(command) + index += 1 + return commands + + +def _tokens(command: str) -> list[str] | None: + try: + lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") + lexer.whitespace_split = True + lexer.commenters = "" + result = list(lexer) + except ValueError: + return None + if not result or any(token in _CONTROL for token in result) or legacy.ENV_ASSIGNMENT.fullmatch(result[0]) or result[0] in _TERMINATORS: + return None + return result + + +def pytest_command(command: str, test_path: str, require_path: bool) -> bool: + parts = _tokens(command) + if not parts: + return False + for start in legacy.PYTEST_STARTS: + if tuple(parts[: len(start)]) == start: + arguments = parts[len(start):] + if any("::" in arg for arg in arguments if not arg.startswith("-")): + return False + return legacy.pytest_safe(arguments, test_path, require_path) + return False + + +def mix_command(command: str, test_path: str, require_path: bool = False) -> bool: + parts = _tokens(command) + if not parts or parts[:2] != ["mix", "test"]: + return False + positive: list[str] = [] + for arg in parts[2:]: + if arg in legacy.SAFE_MIX_FLAGS or arg.startswith(legacy.SAFE_MIX_PREFIXES): + continue + if arg.startswith("-") or ":" in arg: + return False + positive.append(legacy.normalize(arg)) + expected = legacy.normalize(test_path) + return expected in positive if require_path else (not positive or positive == [expected]) + + +def ci_discovery(discovery: Mapping[str, Any], workflow_texts: str | list[str]) -> tuple[bool, list[str]]: + text = "\n".join(workflow_texts) if isinstance(workflow_texts, list) else workflow_texts + scripts = github_run_scripts(text) + commands = [command for script in scripts for command in shell_commands(script)] + strategy = discovery["strategy"] + uses_pytest = strategy == "pytest_default_discovery" or any( + str(pattern).endswith(".py") for pattern in discovery.get("contains_any", []) + ) + if uses_pytest and ( + any(name.startswith("PYTEST_") for name in legacy.yaml_env_names(text)) + or any( + re.search( + r"\bPYTEST_[A-Za-z0-9_]*\b", + "\n".join(legacy.strip_comment(line) for line in script.splitlines()), + ) + for script in scripts + ) + ): + return False, [] + if strategy == "pytest_default_discovery": + matched = any(pytest_command(command, str(discovery["test_path"]), False) for command in commands) + return matched, [str(discovery["command"])] if matched else [] + if strategy == "mix_default_discovery": + matched = any(mix_command(command, str(discovery["test_path"])) for command in commands) + return matched, [str(discovery["command"])] if matched else [] + matches: list[str] = [] + for value in discovery["contains_any"]: + pattern = str(value) + if pattern.endswith(".py") and any(pytest_command(command, pattern, True) for command in commands): + matches.append(pattern) + elif pattern.endswith(".exs") and any(mix_command(command, pattern, True) for command in commands): + matches.append(pattern) + elif not pattern.endswith((".py", ".exs")): + try: + required = shlex.split(pattern) + except ValueError: + continue + if any((parts := _tokens(command)) and parts[: len(required)] == required for command in commands): + matches.append(pattern) + return bool(matches), matches + + +for _name in dir(legacy): + if not _name.startswith("_") and _name not in globals(): + globals()[_name] = getattr(legacy, _name) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_legacy.py b/standards/lotus-family/conformance/lotus_family_workflow_legacy.py new file mode 100644 index 00000000..29a79bf8 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_legacy.py @@ -0,0 +1,427 @@ +from __future__ import annotations + +import fnmatch +import json +import re +import shlex +from pathlib import PurePosixPath +from typing import Any, Mapping + +BARE_KEY = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*\Z") +BLOCK_SCALAR = re.compile(r"([|>])(?:[1-9][+-]?|[+-][1-9]?)?\Z") +ENV_ASSIGNMENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*=.*\Z") +PYTEST_STARTS = (("python", "-m", "pytest"), ("pytest",)) +SAFE_PYTEST_FLAGS = {"-q", "--quiet", "-v", "--verbose", "--strict-markers", "--strict-config", "--disable-warnings"} +SAFE_PYTEST_PREFIXES = ("--junitxml=", "--cov=", "--cov-report=", "--cov-fail-under=", "--color=", "--tb=", "--durations=", "--maxfail=") +FORBIDDEN_PYTEST_FLAGS = {"--collect-only", "--co", "--setup-only", "--pyargs", "-k", "--keyword", "-m", "--markers", "--deselect"} +SAFE_MIX_FLAGS = {"--trace", "--color"} +SAFE_MIX_PREFIXES = ("--seed=", "--max-failures=") +CONTROL_TOKENS = {"&&", "||", ";", "|", "&", "(", ")", "{", "}"} +CONTROL_WORDS = {"if", "then", "elif", "else", "fi", "for", "while", "until", "do", "done", "case", "esac", "select", "function"} + + +def strip_comment(line: str) -> str: + single = double = escaped = False + for index, char in enumerate(line): + if escaped: + escaped = False + elif char == "\\" and not single: + escaped = True + elif char == "'" and not double: + single = not single + elif char == '"' and not single: + double = not double + elif char == "#" and not single and not double: + return line[:index] + return line + + +def indent_of(line: str) -> int | None: + prefix = line[: len(line) - len(line.lstrip(" \t"))] + return None if "\t" in prefix else len(prefix) + + +def decode_key(text: str) -> str | None: + key = text.strip() + if BARE_KEY.fullmatch(key): + return key + if len(key) >= 2 and key[0] == key[-1] == "'": + return key[1:-1].replace("''", "'") or None + if len(key) >= 2 and key[0] == key[-1] == '"': + try: + decoded = json.loads(key) + except json.JSONDecodeError: + return None + return decoded if isinstance(decoded, str) and decoded else None + return None + + +def split_pair(text: str) -> tuple[str, str] | None: + single = double = escaped = False + for index, char in enumerate(text): + if escaped: + escaped = False + elif char == "\\" and double: + escaped = True + elif char == "'" and not double: + single = not single + elif char == '"' and not single: + double = not double + elif char == ":" and not single and not double: + return text[:index], text[index + 1 :] + return None + + +def yaml_header(line: str) -> tuple[int, bool, int, str, str] | None: + indent = indent_of(line) + if indent is None: + return None + text = line[indent:] + if not text or text.startswith("#"): + return None + is_list = False + key_indent = indent + if text.startswith("-"): + match = re.match(r"-([ ]+)(.*)\Z", text) + if not match: + return None + is_list = True + key_indent = indent + 1 + len(match.group(1)) + text = match.group(2) + pair = split_pair(text) + if not pair: + return None + key = decode_key(pair[0]) + return None if key is None else (indent, is_list, key_indent, key, pair[1]) + + +def scalar_indicator(value: str) -> str | None: + match = BLOCK_SCALAR.fullmatch(strip_comment(value).strip()) + return match.group(1) if match else None + + +def inline_scalar(value: str) -> str | None: + text = strip_comment(value).strip() + if not text or text in {"null", "Null", "NULL", "~"}: + return None + if text.startswith("'"): + return text[1:-1].replace("''", "'") if text.endswith("'") else None + if text.startswith('"'): + if not text.endswith('"'): + return None + try: + value = json.loads(text) + except json.JSONDecodeError: + return None + return value if isinstance(value, str) else None + return text + + +def condition_true(value: str) -> bool: + scalar = inline_scalar(value) + return scalar is not None and re.sub(r"\s+", " ", scalar.strip()).lower() in {"true", "${{ true }}"} + + +def scalar_ranges(lines: list[str]) -> dict[int, tuple[int, str]]: + result: dict[int, tuple[int, str]] = {} + index = 0 + while index < len(lines): + header = yaml_header(lines[index]) + style = scalar_indicator(header[4]) if header else None + if not header or not style: + index += 1 + continue + end = index + 1 + while end < len(lines): + if not lines[end].strip(): + end += 1 + continue + indent = indent_of(lines[end]) + if indent is None or indent <= header[2]: + break + end += 1 + result[index] = (end, style) + index = end + return result + + +def scalar_text(lines: list[str], start: int, end: int, style: str) -> str: + body = lines[start + 1 : end] + indents = [indent for line in body if line.strip() and (indent := indent_of(line)) is not None] + if not indents: + return "" + minimum = min(indents) + body = [line[minimum:] if line.strip() else "" for line in body] + if style == "|": + return "\n".join(body) + "\n" + paragraphs: list[str] = [] + current: list[str] = [] + for line in body: + if line: + current.append(line) + elif current: + paragraphs.append(" ".join(current)); current = [] + elif paragraphs: + paragraphs.append("") + if current: + paragraphs.append(" ".join(current)) + return "\n".join(paragraphs) + ("\n" if paragraphs else "") + + +def block_end(lines: list[str], start: int, parent_indent: int) -> int: + index = start + 1 + while index < len(lines): + if not lines[index].strip() or lines[index].lstrip().startswith("#"): + index += 1; continue + indent = indent_of(lines[index]) + if indent is None or indent <= parent_indent: + break + index += 1 + return index + + +def direct_headers(lines: list[str], start: int, end: int, parent_indent: int, scalar_body: set[int]): + found = [] + minimum = None + for row in range(start, end): + if row in scalar_body: + continue + header = yaml_header(lines[row]) + if not header or header[1] or header[2] <= parent_indent: + continue + if minimum is None or header[2] < minimum: + minimum, found = header[2], [(row, header)] + elif header[2] == minimum: + found.append((row, header)) + return found + + +def item_starts(lines: list[str], start: int, end: int, scalar_body: set[int]) -> list[int]: + items: list[int] = [] + minimum = None + for row in range(start, end): + if row in scalar_body: + continue + indent = indent_of(lines[row]) + text = lines[row][indent:] if indent is not None else "" + if indent is not None and text.startswith("-"): + minimum = indent if minimum is None else minimum + if indent == minimum: + items.append(row) + return items + + +def github_run_scripts(text: str) -> list[str]: + lines = text.splitlines() + ranges = scalar_ranges(lines) + scalar_body = {row for start, (end, _) in ranges.items() for row in range(start + 1, end)} + scripts: list[str] = [] + jobs = [(row, h) for row, line in enumerate(lines) if row not in scalar_body and (h := yaml_header(line)) and not h[1] and h[2] == 0 and h[3] == "jobs" and inline_scalar(h[4]) is None and scalar_indicator(h[4]) is None] + for jobs_row, jobs_header in jobs: + jobs_end = block_end(lines, jobs_row, jobs_header[2]) + job_headers = direct_headers(lines, jobs_row + 1, jobs_end, jobs_header[2], scalar_body) + for ji, (job_row, job_header) in enumerate(job_headers): + job_end = job_headers[ji + 1][0] if ji + 1 < len(job_headers) else jobs_end + props = {h[3]: (row, h) for row, h in direct_headers(lines, job_row + 1, job_end, job_header[2], scalar_body)} + runs_on = props.get("runs-on") + if not runs_on or inline_scalar(runs_on[1][4]) is None: + continue + job_if = props.get("if") + if job_if and not condition_true(job_if[1][4]): + continue + steps = props.get("steps") + if not steps or inline_scalar(steps[1][4]) is not None or scalar_indicator(steps[1][4]) is not None: + continue + steps_end = block_end(lines, steps[0], steps[1][2]) + items = item_starts(lines, steps[0] + 1, steps_end, scalar_body) + for si, item_row in enumerate(items): + item_end = items[si + 1] if si + 1 < len(items) else steps_end + first = yaml_header(lines[item_row]) + if not first or not first[1]: + continue + props = {first[3]: (item_row, first)} + for row in range(item_row + 1, item_end): + if row in scalar_body: + continue + header = yaml_header(lines[row]) + if header and not header[1] and header[2] == first[2]: + props[header[3]] = (row, header) + step_if = props.get("if") + if step_if and not condition_true(step_if[1][4]): + continue + run = props.get("run") + if not run: + continue + if run[0] in ranges: + end, style = ranges[run[0]] + scripts.append(scalar_text(lines, run[0], end, style)) + elif (value := inline_scalar(run[1][4])) is not None: + scripts.append(value) + return scripts + + +def yaml_env_names(text: str) -> set[str]: + lines = text.splitlines(); ranges = scalar_ranges(lines) + body = {row for start, (end, _) in ranges.items() for row in range(start + 1, end)} + names: set[str] = set() + for index, line in enumerate(lines): + if index in body: + continue + header = yaml_header(line) + if not header or header[3] != "env": + continue + inline = strip_comment(header[4]).strip() + if inline: + names.update(re.findall(r"\bPYTEST_[A-Za-z0-9_]*\b", inline)); continue + for row in range(index + 1, len(lines)): + if row in body: + continue + if not lines[row].strip() or lines[row].lstrip().startswith("#"): + continue + indent = indent_of(lines[row]) + if indent is None or indent <= header[2]: + break + child = yaml_header(lines[row]) + if child and not child[1] and child[2] > header[2]: + names.add(child[3]) + return names + + +def shell_commands(text: str) -> list[str]: + stripped = "\n".join(strip_comment(line) for line in text.splitlines()) + try: + lexer = shlex.shlex(stripped, posix=True, punctuation_chars=";&|(){}") + lexer.whitespace_split = True; lexer.commenters = "" + tokens = list(lexer) + except ValueError: + return [] + if any(token in CONTROL_TOKENS or token in CONTROL_WORDS for token in tokens): + return [] + lines = text.splitlines(); commands = []; index = 0 + while index < len(lines): + first = strip_comment(lines[index]).strip() + if not first: + index += 1; continue + parts = [first] + while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): + parts[-1] = parts[-1].rstrip()[:-1]; index += 1 + if continuation := strip_comment(lines[index]).strip(): + parts.append(continuation) + commands.append(" ".join(parts)); index += 1 + return commands + + +def tokens(command: str) -> list[str] | None: + try: + lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") + lexer.whitespace_split = True; lexer.commenters = "" + result = list(lexer) + except ValueError: + return None + if not result or any(token in CONTROL_TOKENS for token in result) or ENV_ASSIGNMENT.fullmatch(result[0]): + return None + return result + + +def normalize(value: str) -> str: + value = value.strip().replace("\\", "/") + while value.startswith("./"): + value = value[2:] + return value.rstrip("/") + + +def ignore_covers(value: str, test_path: str, glob: bool = False) -> bool: + value = normalize(value) + if not value: + return True + test = PurePosixPath(test_path) + if glob: + candidates = [test.as_posix(), test.name] + [p.as_posix() for p in test.parents if p != PurePosixPath(".")] + return any(fnmatch.fnmatchcase(candidate, value) for candidate in candidates) + ignored = PurePosixPath(value) + return ignored.is_absolute() or ".." in ignored.parts or ignored == test or ignored in test.parents + + +def pytest_safe(arguments: list[str], test_path: str, require_path: bool) -> bool: + positive = []; index = 0 + while index < len(arguments): + arg = arguments[index] + if arg in FORBIDDEN_PYTEST_FLAGS or arg.startswith(("-k=", "--keyword=", "-m=", "--markers=", "--deselect=")): + return False + if arg.startswith("--ignore=") and ignore_covers(arg.split("=", 1)[1], test_path): + return False + if arg == "--ignore": + if index + 1 >= len(arguments) or ignore_covers(arguments[index + 1], test_path): + return False + index += 1 + elif arg.startswith("--ignore-glob=") and ignore_covers(arg.split("=", 1)[1], test_path, True): + return False + elif arg == "--ignore-glob": + if index + 1 >= len(arguments) or ignore_covers(arguments[index + 1], test_path, True): + return False + index += 1 + elif arg in SAFE_PYTEST_FLAGS or arg.startswith(SAFE_PYTEST_PREFIXES): + pass + elif arg.startswith("-"): + return False + else: + positive.append(normalize(arg.split("::", 1)[0])) + index += 1 + expected = normalize(test_path) + return expected in positive if require_path else not positive + + +def pytest_command(command: str, test_path: str, require_path: bool) -> bool: + parts = tokens(command) + if not parts: + return False + for start in PYTEST_STARTS: + if tuple(parts[: len(start)]) == start: + return pytest_safe(parts[len(start):], test_path, require_path) + return False + + +def mix_command(command: str, test_path: str, require_path: bool = False) -> bool: + parts = tokens(command) + if not parts or parts[:2] != ["mix", "test"]: + return False + positive = [] + for arg in parts[2:]: + if arg in SAFE_MIX_FLAGS or arg.startswith(SAFE_MIX_PREFIXES): + continue + if arg.startswith("-"): + return False + positive.append(normalize(arg.split(":", 1)[0])) + expected = normalize(test_path) + return (expected in positive) if require_path else (not positive or positive == [expected]) + + +def ci_discovery(discovery: Mapping[str, Any], workflow_texts: str | list[str]) -> tuple[bool, list[str]]: + text = "\n".join(workflow_texts) if isinstance(workflow_texts, list) else workflow_texts + scripts = github_run_scripts(text) + commands = [command for script in scripts for command in shell_commands(script)] + strategy = discovery["strategy"] + uses_pytest = strategy == "pytest_default_discovery" or any(str(p).endswith(".py") for p in discovery.get("contains_any", [])) + if uses_pytest and (any(name.startswith("PYTEST_") for name in yaml_env_names(text)) or any(re.search(r"\bPYTEST_[A-Za-z0-9_]*\b", "\n".join(strip_comment(line) for line in script.splitlines())) for script in scripts)): + return False, [] + if strategy == "pytest_default_discovery": + matched = any(pytest_command(command, str(discovery["test_path"]), False) for command in commands) + return matched, [str(discovery["command"])] if matched else [] + if strategy == "mix_default_discovery": + matched = any(mix_command(command, str(discovery["test_path"])) for command in commands) + return matched, [str(discovery["command"])] if matched else [] + matches = [] + for value in discovery["contains_any"]: + pattern = str(value) + if pattern.endswith(".py") and any(pytest_command(command, pattern, True) for command in commands): + matches.append(pattern) + elif pattern.endswith(".exs") and any(mix_command(command, pattern, True) for command in commands): + matches.append(pattern) + elif not pattern.endswith((".py", ".exs")): + try: + required = shlex.split(pattern) + except ValueError: + continue + if any((parts := tokens(command)) and parts[: len(required)] == required for command in commands): + matches.append(pattern) + return bool(matches), matches diff --git a/standards/lotus-family/conformance/test_system_model.py b/standards/lotus-family/conformance/test_system_model.py new file mode 100644 index 00000000..d3e2b4fc --- /dev/null +++ b/standards/lotus-family/conformance/test_system_model.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import copy +import tempfile +import unittest +from pathlib import Path + +from lotus_family_auditor import DRIFT, PASS, UNKNOWN, audit_repository, load_manifest +from lotus_family_system_model import centrality_report, load, traceability, validate_graph, validate_routes + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +GRAPH = ROOT / "causality" / "lotus-family-system-v0.1.json" +ROUTES = ROOT / "causality" / "system-routes-v0.1.json" +MANIFEST = ROOT / "manifest" / "lotus-family-v0.1.json" +SHA = "a" * 40 + + +def workflow(command: str, *, job_extra: str = "", step_extra: str = "", prefix_jobs: str = "") -> str: + body = "\n".join(f" {line}" if line else "" for line in command.splitlines()) + return f"name: CI\njobs:\n{prefix_jobs} test:\n runs-on: ubuntu-latest\n{job_extra} steps:\n - name: Run tests\n{step_extra} run: |\n{body}\n" + + +SCENARIOS = { + "ci-nonrun-001": "name: CI\nenv:\n NOTE: python -m pytest\njobs: {}\n", + "ci-step-skip-001": workflow("python -m pytest", step_extra=" if: ${{ false }}\n"), + "ci-job-skip-001": workflow("python -m pytest", job_extra=" if: ${{ false }}\n"), + "ci-no-runner-001": "name: CI\njobs:\n test:\n steps:\n - run: python -m pytest\n", + "ci-quoted-env-001": 'name: CI\nenv:\n "PYTEST_ADDOPTS": "--ignore=tests/test_lotus_docs_contract.py"\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: python -m pytest\n', + "ci-shell-control-001": workflow("if false; then\n python -m pytest\nfi"), + "cml-subset-001": workflow("python -m pytest tests/test_other.py"), + "pythia-subset-001": workflow("mix test test/unrelated_test.exs"), + "ci-fake-steps-001": "name: CI\nmetadata:\n fake:\n steps:\n - run: python -m pytest\njobs: {}\n", + "ci-custom-shell-001": workflow("python -m pytest", step_extra=" shell: cat {0}\n"), + "ci-terminator-exit-001": workflow("exit 0\npython -m pytest"), + "ci-needs-skip-001": workflow("python -m pytest", job_extra=" needs: gate\n", prefix_jobs=" gate:\n if: ${{ false }}\n runs-on: ubuntu-latest\n steps:\n - run: echo skipped\n"), + "ci-needs-always-pass-001": workflow("python -m pytest", job_extra=" needs: gate\n if: ${{ always() }}\n", prefix_jobs=" gate:\n if: ${{ false }}\n runs-on: ubuntu-latest\n steps:\n - run: echo skipped\n"), + "pythia-line-subset-001": workflow("mix test test/lotus_docs_contract_test.exs:12"), + "ls-node-subset-001": workflow("python -m pytest tests/test_lotus_docs_contract.py::test_pr_evidence_is_bound_to_exact_head"), +} + + +def valid_command(discovery: dict) -> str: + if discovery["strategy"] == "pytest_default_discovery": + return "python -m pytest" + if discovery["strategy"] == "mix_default_discovery": + return "mix test" + pattern = discovery["contains_any"][0] + return f"python -m pytest {pattern}" if pattern.endswith(".py") else f"mix test {pattern}" + + +def materialize(root: Path, config: dict, text: str | None = None) -> Path: + repo = root / config["snapshot_dir"] + terms: dict[str, list[str]] = {} + for check in config["file_checks"]: + terms.setdefault(check["path"], []).extend(check["contains_all"]) + for name, values in terms.items(): + path = repo / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(dict.fromkeys(values)) + "\n", encoding="utf-8") + discovery = config["ci_discovery"] + path = repo / discovery["workflow_paths"][0] + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text or workflow(valid_command(discovery)), encoding="utf-8") + return repo + + +class SystemModelTest(unittest.TestCase): + def setUp(self): + self.graph = load(GRAPH) + self.route_model = load(ROUTES) + self.routes = validate_routes(self.graph, self.route_model) + self.manifest = load_manifest(MANIFEST) + + def test_graph_has_five_dimensions_centers_and_trajectories(self): + view = validate_graph(self.graph) + self.assertEqual(len(view["nodes"]), 46) + self.assertEqual(len(view["edges"]), 62) + self.assertEqual(len(self.routes), 25) + self.assertEqual(set(self.graph["dimensions"]), {"causal", "spatial", "temporal", "hierarchy", "trajectory"}) + + def test_centrality_is_review_priority_not_authority(self): + report = centrality_report(self.graph, self.routes) + self.assertIn("never ownership or authority", report["meaning"]) + self.assertEqual(len(report["nodes"]), 46) + + def test_traceability_is_derived_from_routes(self): + ledger = traceability(self.routes) + for route in self.routes: + self.assertIn(f"`{route['id']}`", ledger) + + def test_runtime_routes_reach_expected_outcomes(self): + runtime = [route for route in self.routes if route["outcome"] in {PASS, DRIFT, UNKNOWN}] + for route in runtime: + with self.subTest(route=route["id"]): + manifest = copy.deepcopy(self.manifest) + config = next(row for row in manifest["repositories"] if row["id"] == route["repo"]) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + if route["scenario"] != "missing_snapshot": + repo = materialize(root, config, SCENARIOS.get(route["scenario"])) + if route["scenario"] == "missing_term": + check = config["file_checks"][0] + path = repo / check["path"] + path.write_text(path.read_text(encoding="utf-8").replace(check["contains_all"][0], ""), encoding="utf-8") + elif route["scenario"] == "invalid_manifest": + config["file_checks"][0]["contains_all"] = [] + result = audit_repository( + manifest, + repository_id=route["repo"], + snapshot_root=root, + repository_ref="refs/heads/main", + commit_sha="main" if route["scenario"] == "invalid_commit" else SHA, + ) + self.assertEqual((result["outcome"], result["reason_code"]), (route["outcome"], route["reason"])) + self.assertFalse(result["authority"]["grants_merge"]) + + def test_merge_trajectory_never_grants_merge_authority(self): + model_routes = [route for route in self.routes if route["outcome"] == "MODEL"] + self.assertEqual({route["id"] for route in model_routes}, {"MERGE-ELIGIBLE-001", "MERGE-STALE-001"}) + self.assertTrue(all(route["path"][-1] in {"authority.advisory_only", "state.merge_blocked"} for route in model_routes)) + + +if __name__ == "__main__": + unittest.main() From c8cff73a78628f958c42232ee1b0fd6fa619b010 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:42:25 +0300 Subject: [PATCH 29/79] fix: bind spacetime model to verified provenance --- standards/lotus-family/README.md | 36 +- .../lotus-family/causality/SYSTEM-MODEL.md | 55 ++- .../causality/lotus-family-system-v0.1.json | 2 +- .../causality/system-routes-v0.1.json | 2 +- .../conformance/lotus_family_runtime.py | 230 ++++++++-- .../conformance/lotus_family_system_model.py | 94 +++- .../lotus_family_workflow_hardened.py | 332 ++++++++++---- .../conformance/test_causality_model.py | 183 ++++++-- .../conformance/test_system_model.py | 423 +++++++++++++++--- 9 files changed, 1083 insertions(+), 274 deletions(-) diff --git a/standards/lotus-family/README.md b/standards/lotus-family/README.md index d375de4c..c091678c 100644 --- a/standards/lotus-family/README.md +++ b/standards/lotus-family/README.md @@ -20,23 +20,35 @@ caller-provided claims recorded in evidence. Results expose `identity_assurance.mode = caller_claim_only` so consumers cannot mistake these claims for verified provenance. -Remote identity verification, trusted materialization, and evidence attestation -remain follow-up work. Until then, `PASS` means conformance of the supplied +SHA-256 evidence binds each check to the exact bytes read and evaluated from the +supplied snapshot. It does **not** prove that those bytes came from the claimed +remote repository, ref, or commit. Independent trusted materialization and +provenance verification are required before evidence may be called fresh for an +exact head. + +Until that verification exists, `PASS` means conformance of the supplied snapshot contents, not cryptographic proof that the snapshot came from the claimed remote commit. -## Causal testing model +## Causal spacetime testing model + +The test model has two compatible layers: -The primary test model lives in: +- `causality/lotus-family-causality-v0.1.json` and + `causality/test-paths-v0.1.json` preserve the original causal routes; +- `causality/lotus-family-system-v0.1.json` and + `causality/system-routes-v0.1.json` add spatial, temporal, hierarchical, and + trajectory views. -- `causality/lotus-family-causality-v0.1.json` — causes, controls, risks, evidence, - outcomes, and authority boundaries; -- `causality/test-paths-v0.1.json` — executable end-to-end routes through that graph; -- `causality/TRACEABILITY.md` — a reviewable matrix derived from the routes. +The system graph separates bounded snapshot `PASS` from independently verified +exact-head freshness and merge eligibility. Centrality means review priority and +blast radius only; it never grants ownership, approval, execution, delivery, or +merge authority. -Every new blocker must add or reuse a causal node and include an executable route. -This prevents isolated regression tests from hiding missing relationships between -workflow structure, command execution, test selection, evidence, and verdicts. +Every new blocker must add or reuse a graph node and include an executable route +when runtime behavior is involved. This prevents isolated regression tests from +hiding missing relationships between workflow structure, inherited execution +context, test selection, evidence, and verdicts. ## Boundary @@ -44,7 +56,7 @@ The auditor is read-only and `audit_only`. Its result does not grant ownership, approval, execution, delivery, deployment, or merge authority. It does not fetch repositories, call GitHub, merge pull requests, or deploy -software. An integration materializes the repository snapshot it wants audited. +software. An integration materializes the repository snapshot it wants to audit. ## Snapshot layout diff --git a/standards/lotus-family/causality/SYSTEM-MODEL.md b/standards/lotus-family/causality/SYSTEM-MODEL.md index d9ea7478..4f616432 100644 --- a/standards/lotus-family/causality/SYSTEM-MODEL.md +++ b/standards/lotus-family/causality/SYSTEM-MODEL.md @@ -1,37 +1,68 @@ # Lotus causal spacetime system model -The system model extends the original causal test graph with four additional views while keeping one audit-only source of truth. +The system model extends the original causal test graph with four additional +views while keeping one audit-only source of truth. ## Five dimensions - **Causal** — why a condition changes risk or outcome. -- **Spatial** — where evidence exists: repository, file, workflow, job, dependency graph, step, shell, test scope, evidence bundle, review, or merge gate. -- **Temporal** — which state must precede another and when evidence becomes stale after the exact head changes. -- **Hierarchy** — how goals, invariants, controls, evidence, outcomes, gates, and authority boundaries relate. -- **Trajectory** — how the system moves toward `PASS`, fail-closed `DRIFT`, evidence staleness, or merge eligibility. +- **Spatial** — where evidence exists: repository, file, workflow, job, + dependency graph, step, inherited shell, working directory, test scope, + evidence bundle, review, or merge gate. +- **Temporal** — which state must precede another and when independently bound + exact-head evidence becomes stale. +- **Hierarchy** — how goals, invariants, controls, evidence, outcomes, gates, and + authority boundaries relate. +- **Trajectory** — how the system moves toward bounded snapshot `PASS`, + fail-closed `DRIFT`, stale evidence, or merge eligibility. Machine-readable sources: -- `lotus-family-system-v0.1.json` — 46 nodes, 62 unique directed relationships, centers, and four canonical trajectories; -- `system-routes-v0.1.json` — 25 connected routes, including runtime regressions and governance-only merge trajectories. +- `lotus-family-system-v0.1.json` — 49 nodes, 64 unique directed + relationships, centers, and four canonical trajectories; +- `system-routes-v0.1.json` — 32 connected routes, including runtime + regressions and governance-only merge trajectories. ## Centers without centralized authority -The model has semantic and evidence centers: current exact head, invariant set, same-byte hashes, and the advisory authority boundary. They centralize identity, meaning, and evidence consistency. They do **not** centralize execution or decision power. +The model has separate semantic, snapshot-evidence, and governance centers: +the invariant set, same-byte hashes, independently verified exact head, and the +advisory authority boundary. -Centrality scores mean review priority and blast radius only. A highly connected node deserves stronger tests and independent review; it does not gain ownership or approval authority. +Same-byte hashing proves which supplied bytes were evaluated. It does not prove +remote provenance. `center.exact_head` participates only in governance +trajectories after `gate.provenance_verified`; runtime audit `PASS` routes retain +`limitation.identity_unverified`. + +Centrality scores mean review priority and blast radius only. A highly connected +node deserves stronger tests and independent review; it does not gain ownership +or approval authority. ## Time and merge separation -`PASS` belongs to the audit trajectory. Merge eligibility is a separate trajectory requiring green CI, green security, a fresh exact-head review, and no actionable blockers. Even `state.merge_eligible` terminates at `authority.advisory_only`. +`PASS` belongs to the bounded snapshot-audit trajectory. Merge eligibility is a +separate trajectory requiring independent provenance binding, fresh exact-head +evidence, green CI, green security, a fresh exact-head review, and no actionable +blockers. Even `state.merge_eligible` terminates at +`authority.advisory_only`. + +A head change moves independently bound evidence to `time.evidence_stale`, which +leads to `state.merge_blocked` until checks and review are rerun on the new exact +head. + +## Effective execution context -A head change moves prior evidence to `time.evidence_stale`, which leads to `state.merge_blocked` until checks and review are rerun on the new exact head. +GitHub Actions `defaults.run` values are resolved through workflow, job, and step +scope. Step-level values override job defaults, and job defaults override +workflow defaults. Unknown shells and non-root or dynamic working directories +fail closed because raw command text alone cannot prove which tests execute. ## New blocker rule Every newly discovered blocker must: 1. add or reuse a system node; -2. add a directed relationship if the causal, spatial, temporal, or hierarchical link is new; +2. add a directed relationship if the causal, spatial, temporal, or hierarchical + link is new; 3. include an executable route when runtime behavior is involved; 4. preserve the boundary `PASS != APPROVED != MERGED`. diff --git a/standards/lotus-family/causality/lotus-family-system-v0.1.json b/standards/lotus-family/causality/lotus-family-system-v0.1.json index 258686d7..955a2d9a 100644 --- a/standards/lotus-family/causality/lotus-family-system-v0.1.json +++ b/standards/lotus-family/causality/lotus-family-system-v0.1.json @@ -1 +1 @@ -{"schema_version":"pythia.lotus_system_graph.compact.v0.1","model_kind":"causal_spacetime_hierarchy_trajectory","authority":"audit_only","scope":"Lotus Family contract, executable CI discovery, evidence freshness, and merge eligibility separation","dimensions":{"causal":"Why a condition changes risk or outcome","spatial":"Where a condition sits in repository, workflow, job, step, shell, test, evidence, or review topology","temporal":"Which state must precede another and when evidence becomes stale","hierarchy":"How goals, invariants, controls, evidence, outcomes, gates, and authority relate","trajectory":"How the system moves from input to verdict and separately toward merge eligibility"},"temporal_phases":["anchor","input","validation","topology","execution","evidence","verdict","governance","merge"],"node_columns":["id","type","space","time","hierarchy","centrality_role"],"nodes":[["center.exact_head","center","family","anchor","goal","anchor"],["center.invariant_set","center","family","validation","invariant","hub"],["input.snapshot_present","precondition","repository","input","input","ordinary"],["input.snapshot_missing","precondition","repository","input","input","ordinary"],["input.identity_claims_well_formed","claim","repository","input","input","ordinary"],["input.identity_claims_invalid","claim","repository","input","input","ordinary"],["limitation.identity_unverified","limitation","integration","validation","limitation","ordinary"],["control.manifest_valid","control","family","validation","control","ordinary"],["control.manifest_invalid","control","family","validation","control","ordinary"],["control.contract_terms_present","control","file","validation","control","ordinary"],["control.contract_terms_missing","control","file","validation","control","ordinary"],["control.workflow_job_runnable","control","job","topology","control","ordinary"],["control.workflow_job_not_runnable","control","job","topology","control","ordinary"],["control.dependencies_proven","control","job_graph","topology","control","ordinary"],["control.dependencies_unproven","control","job_graph","topology","control","ordinary"],["control.step_enabled","control","step","topology","control","ordinary"],["control.step_skipped","control","step","topology","control","ordinary"],["control.direct_run","control","step","execution","control","ordinary"],["control.non_run_text","control","workflow","execution","control","ordinary"],["control.shell_semantics_known","control","shell","execution","control","ordinary"],["control.shell_semantics_unknown","control","shell","execution","control","ordinary"],["control.test_env_clean","control","environment","execution","control","ordinary"],["control.test_env_override","control","environment","execution","control","ordinary"],["control.shell_straight_line","control","script","execution","control","ordinary"],["control.shell_control_flow","control","script","execution","control","ordinary"],["control.no_terminating_builtin","control","script","execution","control","ordinary"],["control.terminating_builtin","control","script","execution","control","ordinary"],["control.full_test_scope","control","test_scope","execution","control","ordinary"],["control.partial_test_scope","control","test_scope","execution","control","ordinary"],["control.contract_test_selected","control","test_scope","execution","control","ordinary"],["control.contract_test_excluded","control","test_scope","execution","control","ordinary"],["evidence.same_bytes_hashed","evidence","evidence_bundle","evidence","evidence","hub"],["time.evidence_fresh","temporal","evidence_bundle","evidence","evidence","hub"],["time.evidence_stale","temporal","evidence_bundle","evidence","evidence","ordinary"],["risk.false_ci_pass","risk","family","verdict","risk","ordinary"],["risk.identity_overclaim","risk","integration","verdict","risk","ordinary"],["outcome.pass","outcome","audit","verdict","outcome","hub"],["outcome.drift","outcome","audit","verdict","outcome","ordinary"],["outcome.unknown","outcome","audit","verdict","outcome","ordinary"],["gate.ci_green","gate","github","evidence","gate","ordinary"],["gate.security_green","gate","github","evidence","gate","ordinary"],["gate.review_fresh","gate","review","governance","gate","ordinary"],["gate.no_actionable_blockers","gate","review","governance","gate","ordinary"],["state.merge_eligible","state","merge","merge","state","ordinary"],["state.merge_blocked","state","merge","merge","state","ordinary"],["authority.advisory_only","boundary","family","governance","authority","boundary"]],"edge_columns":["id","source","target","relation","dimension"],"edges":[["E01","center.exact_head","input.snapshot_present","anchors","temporal"],["E02","center.exact_head","input.snapshot_missing","anchors","temporal"],["E03","input.snapshot_present","input.identity_claims_well_formed","enables","causal"],["E04","input.snapshot_present","input.identity_claims_invalid","may_reveal","causal"],["E05","input.snapshot_missing","outcome.unknown","causes","causal"],["E06","input.identity_claims_invalid","outcome.unknown","causes","causal"],["E07","input.identity_claims_well_formed","limitation.identity_unverified","retains_limitation","causal"],["E08","limitation.identity_unverified","center.invariant_set","permits_bounded_audit","hierarchy"],["E09","limitation.identity_unverified","risk.identity_overclaim","creates_if_misrepresented","causal"],["E10","risk.identity_overclaim","authority.advisory_only","is_bounded_by","authority"],["E11","center.invariant_set","control.manifest_valid","defines","hierarchy"],["E12","center.invariant_set","control.manifest_invalid","may_reveal","hierarchy"],["E13","control.manifest_invalid","outcome.unknown","causes","causal"],["E14","control.manifest_valid","control.contract_terms_present","enables_check","causal"],["E15","control.manifest_valid","control.contract_terms_missing","may_reveal","causal"],["E16","control.contract_terms_missing","outcome.drift","causes","causal"],["E17","control.contract_terms_present","control.workflow_job_runnable","enables_ci_topology","spatial"],["E18","control.contract_terms_present","control.workflow_job_not_runnable","may_reveal","spatial"],["E19","control.workflow_job_not_runnable","control.non_run_text","may_reveal","spatial"],["E20","control.non_run_text","risk.false_ci_pass","would_create_without_guard","causal"],["E21","control.workflow_job_not_runnable","risk.false_ci_pass","would_create_without_guard","causal"],["E22","control.workflow_job_runnable","control.dependencies_proven","requires","temporal"],["E23","control.workflow_job_runnable","control.dependencies_unproven","may_reveal","temporal"],["E24","control.dependencies_unproven","risk.false_ci_pass","would_create_without_guard","causal"],["E25","control.dependencies_proven","control.step_enabled","enables","spatial"],["E26","control.dependencies_proven","control.step_skipped","may_reveal","spatial"],["E27","control.step_skipped","risk.false_ci_pass","would_create_without_guard","causal"],["E28","control.step_enabled","control.direct_run","enables","spatial"],["E29","control.step_enabled","control.non_run_text","may_reveal","spatial"],["E31","control.direct_run","control.shell_semantics_known","requires","spatial"],["E32","control.direct_run","control.shell_semantics_unknown","may_reveal","spatial"],["E33","control.shell_semantics_unknown","risk.false_ci_pass","would_create_without_guard","causal"],["E34","control.shell_semantics_known","control.test_env_clean","requires","causal"],["E35","control.shell_semantics_known","control.test_env_override","may_reveal","causal"],["E36","control.test_env_override","risk.false_ci_pass","would_create_without_guard","causal"],["E37","control.test_env_clean","control.shell_straight_line","requires","causal"],["E38","control.test_env_clean","control.shell_control_flow","may_reveal","causal"],["E39","control.shell_control_flow","risk.false_ci_pass","would_create_without_guard","causal"],["E40","control.shell_straight_line","control.no_terminating_builtin","requires","temporal"],["E41","control.shell_straight_line","control.terminating_builtin","may_reveal","temporal"],["E42","control.terminating_builtin","risk.false_ci_pass","would_create_without_guard","causal"],["E43","control.no_terminating_builtin","control.full_test_scope","enables","causal"],["E44","control.no_terminating_builtin","control.partial_test_scope","may_reveal","causal"],["E45","control.partial_test_scope","control.contract_test_excluded","causes","causal"],["E46","control.contract_test_excluded","risk.false_ci_pass","would_create_without_guard","causal"],["E47","control.full_test_scope","control.contract_test_selected","proves","causal"],["E48","risk.false_ci_pass","outcome.drift","is_prevented_by_fail_closed_result","causal"],["E49","control.contract_test_selected","evidence.same_bytes_hashed","enables_evidence","causal"],["E50","evidence.same_bytes_hashed","time.evidence_fresh","binds_to_current_head","temporal"],["E51","evidence.same_bytes_hashed","time.evidence_stale","may_be_superseded","temporal"],["E52","time.evidence_fresh","outcome.pass","supports","causal"],["E53","outcome.pass","authority.advisory_only","remains_bounded_by","authority"],["E54","time.evidence_stale","state.merge_blocked","blocks","temporal"],["E55","center.exact_head","gate.ci_green","anchors","temporal"],["E56","gate.ci_green","gate.security_green","coexists_with","temporal"],["E57","gate.security_green","outcome.pass","combines_with","temporal"],["E58","outcome.pass","gate.review_fresh","requires_for_merge","temporal"],["E59","gate.review_fresh","gate.no_actionable_blockers","enables","governance"],["E60","gate.no_actionable_blockers","state.merge_eligible","enables","governance"],["E61","state.merge_eligible","authority.advisory_only","does_not_grant_authority","authority"],["E62","outcome.drift","state.merge_blocked","blocks","governance"],["E63","outcome.unknown","state.merge_blocked","blocks","governance"]],"centers":[["center.exact_head","evidence_anchor","identity and freshness, not authority"],["center.invariant_set","semantic_anchor","meaning and coverage"],["evidence.same_bytes_hashed","evidence_hub","evaluated bytes and hashes"],["authority.advisory_only","authority_boundary","limitations without granting execution"]],"trajectories":[["audit-pass",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"Bounded snapshot audit reaches PASS without granting authority"],["false-pass-prevention",["control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift","state.merge_blocked"],"Ambiguous execution semantics fail closed"],["evidence-staleness",["evidence.same_bytes_hashed","time.evidence_stale","state.merge_blocked"],"Head changes invalidate downstream merge evidence"],["merge-eligibility",["center.exact_head","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"Independent gates converge on eligibility while authority remains human"]],"centrality_policy":{"meaning":"Centrality identifies review priority and blast radius, never ownership or authority.","critical_nodes":["center.exact_head","center.invariant_set","control.direct_run","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"minimum_route_coverage":2},"coverage_policy":{"required_node_types":["center","control","evidence","temporal","outcome","boundary"],"route_file":"system-routes-v0.1.json","new_blocker_rule":"Every new blocker must add or reuse a graph node and at least one executable route.","derived_views":["causal","spatial","temporal","hierarchy","trajectory","traceability"]}} +{"schema_version":"pythia.lotus_system_graph.compact.v0.1","graph_id":"lotus-family-system-v0.1.json","model_kind":"causal_spacetime_hierarchy_trajectory","authority":"audit_only","scope":"Lotus Family contract, executable CI discovery, bounded snapshot evidence, and merge eligibility separation","dimensions":{"causal":"Why a condition changes risk or outcome","spatial":"Where a condition sits in repository, workflow, job, step, shell, working directory, test, evidence, or review topology","temporal":"Which state must precede another and when independently bound evidence becomes stale","hierarchy":"How goals, invariants, controls, evidence, outcomes, gates, and authority relate","trajectory":"How the system moves from input to verdict and separately toward merge eligibility"},"temporal_phases":["anchor","input","validation","topology","execution","evidence","verdict","governance","merge"],"node_columns":["id","type","space","time","hierarchy","centrality_role"],"nodes":[["center.exact_head","center","family","anchor","goal","anchor"],["center.invariant_set","center","family","validation","invariant","hub"],["input.snapshot_present","precondition","repository","input","input","ordinary"],["input.snapshot_missing","precondition","repository","input","input","ordinary"],["input.identity_claims_well_formed","claim","repository","input","input","ordinary"],["input.identity_claims_invalid","claim","repository","input","input","ordinary"],["limitation.identity_unverified","limitation","integration","validation","limitation","ordinary"],["control.manifest_valid","control","family","validation","control","ordinary"],["control.manifest_invalid","control","family","validation","control","ordinary"],["control.contract_terms_present","control","file","validation","control","ordinary"],["control.contract_terms_missing","control","file","validation","control","ordinary"],["control.workflow_job_runnable","control","job","topology","control","ordinary"],["control.workflow_job_not_runnable","control","job","topology","control","ordinary"],["control.dependencies_proven","control","job_graph","topology","control","ordinary"],["control.dependencies_unproven","control","job_graph","topology","control","ordinary"],["control.step_enabled","control","step","topology","control","ordinary"],["control.step_skipped","control","step","topology","control","ordinary"],["control.direct_run","control","step","execution","control","ordinary"],["control.non_run_text","control","workflow","execution","control","ordinary"],["control.shell_semantics_known","control","shell","execution","control","ordinary"],["control.shell_semantics_unknown","control","shell","execution","control","ordinary"],["control.working_directory_repo_root","control","working_directory","execution","control","ordinary"],["control.working_directory_non_root","control","working_directory","execution","control","ordinary"],["control.test_env_clean","control","environment","execution","control","ordinary"],["control.test_env_override","control","environment","execution","control","ordinary"],["control.shell_straight_line","control","script","execution","control","ordinary"],["control.shell_control_flow","control","script","execution","control","ordinary"],["control.no_terminating_builtin","control","script","execution","control","ordinary"],["control.terminating_builtin","control","script","execution","control","ordinary"],["control.full_test_scope","control","test_scope","execution","control","ordinary"],["control.partial_test_scope","control","test_scope","execution","control","ordinary"],["control.contract_test_selected","control","test_scope","execution","control","ordinary"],["control.contract_test_excluded","control","test_scope","execution","control","ordinary"],["evidence.same_bytes_hashed","evidence","evidence_bundle","evidence","evidence","hub"],["gate.provenance_verified","gate","integration","evidence","gate","ordinary"],["time.evidence_fresh","temporal","evidence_bundle","evidence","evidence","hub"],["time.evidence_stale","temporal","evidence_bundle","evidence","evidence","ordinary"],["risk.false_ci_pass","risk","family","verdict","risk","ordinary"],["risk.identity_overclaim","risk","integration","verdict","risk","ordinary"],["outcome.pass","outcome","audit","verdict","outcome","hub"],["outcome.drift","outcome","audit","verdict","outcome","ordinary"],["outcome.unknown","outcome","audit","verdict","outcome","ordinary"],["gate.ci_green","gate","github","evidence","gate","ordinary"],["gate.security_green","gate","github","evidence","gate","ordinary"],["gate.review_fresh","gate","review","governance","gate","ordinary"],["gate.no_actionable_blockers","gate","review","governance","gate","ordinary"],["state.merge_eligible","state","merge","merge","state","ordinary"],["state.merge_blocked","state","merge","merge","state","ordinary"],["authority.advisory_only","boundary","family","governance","authority","boundary"]],"edge_columns":["id","source","target","relation","dimension"],"edges":[["E01","input.snapshot_present","input.identity_claims_well_formed","enables","causal"],["E02","input.snapshot_present","input.identity_claims_invalid","may_reveal","causal"],["E03","input.snapshot_missing","outcome.unknown","causes","causal"],["E04","input.identity_claims_invalid","outcome.unknown","causes","causal"],["E05","input.identity_claims_well_formed","limitation.identity_unverified","retains_limitation","causal"],["E06","limitation.identity_unverified","center.invariant_set","permits_bounded_audit","hierarchy"],["E07","limitation.identity_unverified","risk.identity_overclaim","creates_if_misrepresented","causal"],["E08","risk.identity_overclaim","authority.advisory_only","is_bounded_by","authority"],["E09","center.invariant_set","control.manifest_valid","defines","hierarchy"],["E10","center.invariant_set","control.manifest_invalid","may_reveal","hierarchy"],["E11","control.manifest_invalid","outcome.unknown","causes","causal"],["E12","control.manifest_valid","control.contract_terms_present","enables_check","causal"],["E13","control.manifest_valid","control.contract_terms_missing","may_reveal","causal"],["E14","control.contract_terms_missing","outcome.drift","causes","causal"],["E15","control.contract_terms_present","control.workflow_job_runnable","enables_ci_topology","spatial"],["E16","control.contract_terms_present","control.workflow_job_not_runnable","may_reveal","spatial"],["E17","control.workflow_job_not_runnable","control.non_run_text","may_reveal","spatial"],["E18","control.non_run_text","risk.false_ci_pass","would_create_without_guard","causal"],["E19","control.workflow_job_not_runnable","risk.false_ci_pass","would_create_without_guard","causal"],["E20","control.workflow_job_runnable","control.dependencies_proven","requires","temporal"],["E21","control.workflow_job_runnable","control.dependencies_unproven","may_reveal","temporal"],["E22","control.dependencies_unproven","risk.false_ci_pass","would_create_without_guard","causal"],["E23","control.dependencies_proven","control.step_enabled","enables","spatial"],["E24","control.dependencies_proven","control.step_skipped","may_reveal","spatial"],["E25","control.step_skipped","risk.false_ci_pass","would_create_without_guard","causal"],["E26","control.step_enabled","control.direct_run","enables","spatial"],["E27","control.step_enabled","control.non_run_text","may_reveal","spatial"],["E28","control.direct_run","control.shell_semantics_known","requires","spatial"],["E29","control.direct_run","control.shell_semantics_unknown","may_reveal","spatial"],["E30","control.shell_semantics_unknown","risk.false_ci_pass","would_create_without_guard","causal"],["E31","control.shell_semantics_known","control.working_directory_repo_root","requires","spatial"],["E32","control.shell_semantics_known","control.working_directory_non_root","may_reveal","spatial"],["E33","control.working_directory_non_root","risk.false_ci_pass","would_create_without_guard","causal"],["E34","control.working_directory_repo_root","control.test_env_clean","requires","causal"],["E35","control.working_directory_repo_root","control.test_env_override","may_reveal","causal"],["E36","control.test_env_override","risk.false_ci_pass","would_create_without_guard","causal"],["E37","control.test_env_clean","control.shell_straight_line","requires","causal"],["E38","control.test_env_clean","control.shell_control_flow","may_reveal","causal"],["E39","control.shell_control_flow","risk.false_ci_pass","would_create_without_guard","causal"],["E40","control.shell_straight_line","control.no_terminating_builtin","requires","temporal"],["E41","control.shell_straight_line","control.terminating_builtin","may_reveal","temporal"],["E42","control.terminating_builtin","risk.false_ci_pass","would_create_without_guard","causal"],["E43","control.no_terminating_builtin","control.full_test_scope","enables","causal"],["E44","control.no_terminating_builtin","control.partial_test_scope","may_reveal","causal"],["E45","control.partial_test_scope","control.contract_test_excluded","causes","causal"],["E46","control.contract_test_excluded","risk.false_ci_pass","would_create_without_guard","causal"],["E47","control.full_test_scope","control.contract_test_selected","proves","causal"],["E48","risk.false_ci_pass","outcome.drift","is_prevented_by_fail_closed_result","causal"],["E49","control.contract_test_selected","evidence.same_bytes_hashed","enables_snapshot_evidence","causal"],["E50","evidence.same_bytes_hashed","outcome.pass","supports_snapshot_content_only","causal"],["E51","outcome.pass","authority.advisory_only","remains_bounded_by","authority"],["E52","center.exact_head","gate.provenance_verified","requires_independent_binding","temporal"],["E53","gate.provenance_verified","time.evidence_fresh","establishes","temporal"],["E54","gate.provenance_verified","time.evidence_stale","may_be_superseded","temporal"],["E55","time.evidence_stale","state.merge_blocked","blocks","temporal"],["E56","time.evidence_fresh","gate.ci_green","enables_exact_head_gate","temporal"],["E57","gate.ci_green","gate.security_green","coexists_with","temporal"],["E58","gate.security_green","outcome.pass","combines_with_bounded_audit","temporal"],["E59","outcome.pass","gate.review_fresh","requires_for_merge","temporal"],["E60","gate.review_fresh","gate.no_actionable_blockers","enables","governance"],["E61","gate.no_actionable_blockers","state.merge_eligible","enables","governance"],["E62","state.merge_eligible","authority.advisory_only","does_not_grant_authority","authority"],["E63","outcome.drift","state.merge_blocked","blocks","governance"],["E64","outcome.unknown","state.merge_blocked","blocks","governance"]],"centers":[["center.exact_head","governance_anchor","exact-head freshness only after independent provenance verification"],["center.invariant_set","semantic_anchor","meaning and coverage"],["evidence.same_bytes_hashed","snapshot_evidence_hub","evaluated bytes and hashes; not remote provenance"],["authority.advisory_only","authority_boundary","limitations without granting execution"]],"trajectories":[["audit-pass",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"Bounded snapshot audit reaches PASS without claiming remote provenance or granting authority"],["false-pass-prevention",["control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift","state.merge_blocked"],"Ambiguous execution semantics fail closed"],["evidence-staleness",["center.exact_head","gate.provenance_verified","time.evidence_stale","state.merge_blocked"],"Independently bound exact-head evidence becomes stale after supersession"],["merge-eligibility",["center.exact_head","gate.provenance_verified","time.evidence_fresh","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"Independent provenance and review gates converge on eligibility while authority remains human"]],"centrality_policy":{"meaning":"Centrality identifies review priority and blast radius, never ownership or authority.","critical_nodes":["center.exact_head","center.invariant_set","control.direct_run","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"minimum_route_coverage":2},"coverage_policy":{"required_node_types":["center","control","evidence","temporal","outcome","boundary"],"route_file":"system-routes-v0.1.json","new_blocker_rule":"Every new blocker must add or reuse a graph node and at least one executable route.","derived_views":["causal","spatial","temporal","hierarchy","trajectory","traceability"]}} diff --git a/standards/lotus-family/causality/system-routes-v0.1.json b/standards/lotus-family/causality/system-routes-v0.1.json index c35307c0..f884460e 100644 --- a/standards/lotus-family/causality/system-routes-v0.1.json +++ b/standards/lotus-family/causality/system-routes-v0.1.json @@ -1 +1 @@ -{"schema_version":"pythia.lotus_system_routes.compact.v0.1","graph":"lotus-family-system-v0.1.json","route_columns":["id","repo","scenario","path","trajectory","outcome","reason"],"routes":[["CML-PASS-001","cml","valid",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["LS-PASS-001","ls","valid",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-PASS-001","pythia","valid",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-NONRUN-001","cml","ci-nonrun-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","control.non_run_text","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SKIP-001","cml","ci-step-skip-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_skipped","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-SKIP-001","cml","ci-job-skip-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NO-RUNNER-001","cml","ci-no-runner-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-QUOTED-ENV-001","cml","ci-quoted-env-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_override","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-CONTROL-001","cml","ci-shell-control-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_control_flow","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CML-SUBSET-001","cml","cml-subset-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["PYTHIA-SUBSET-001","pythia","pythia-subset-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-FAKE-STEPS-001","cml","ci-fake-steps-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-CUSTOM-SHELL-001","cml","ci-custom-shell-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-TERMINATOR-EXIT-001","cml","ci-terminator-exit-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-SKIP-001","cml","ci-needs-skip-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-ALWAYS-PASS-001","cml","ci-needs-always-pass-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","time.evidence_fresh","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-LINE-SUBSET-001","pythia","pythia-line-subset-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["LS-NODE-SUBSET-001","ls","ls-node-subset-001",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["IDENTITY-INVALID-001","cml","invalid_commit",["center.exact_head","input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","COMMIT_SHA_INVALID"],["SNAPSHOT-MISSING-001","cml","missing_snapshot",["center.exact_head","input.snapshot_missing","outcome.unknown"],"audit","UNKNOWN","SNAPSHOT_UNAVAILABLE"],["CONTRACT-DRIFT-001","cml","missing_term",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_missing","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["IDENTITY-LIMIT-001","cml","valid_identity",["center.exact_head","input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","risk.identity_overclaim","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["MANIFEST-INVALID-001","cml","invalid_manifest",["center.invariant_set","control.manifest_invalid","outcome.unknown"],"audit","UNKNOWN","MANIFEST_INVALID"],["MERGE-ELIGIBLE-001","cml","valid",["center.exact_head","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"merge","MODEL","MODEL_ONLY"],["MERGE-STALE-001","cml","valid",["evidence.same_bytes_hashed","time.evidence_stale","state.merge_blocked"],"merge","MODEL","MODEL_ONLY"]]} +{"schema_version":"pythia.lotus_system_routes.compact.v0.1","graph":"lotus-family-system-v0.1.json","route_columns":["id","repo","scenario","path","trajectory","outcome","reason"],"routes":[["CML-PASS-001","cml","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["LS-PASS-001","ls","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-PASS-001","pythia","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-NONRUN-001","cml","ci-nonrun-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","control.non_run_text","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SKIP-001","cml","ci-step-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_skipped","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-SKIP-001","cml","ci-job-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NO-RUNNER-001","cml","ci-no-runner-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-QUOTED-ENV-001","cml","ci-quoted-env-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_override","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-CONTROL-001","cml","ci-shell-control-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_control_flow","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CML-SUBSET-001","cml","cml-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["PYTHIA-SUBSET-001","pythia","pythia-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-FAKE-STEPS-001","cml","ci-fake-steps-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-CUSTOM-SHELL-001","cml","ci-custom-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-TERMINATOR-EXIT-001","cml","ci-terminator-exit-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-SKIP-001","cml","ci-needs-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-ALWAYS-PASS-001","cml","ci-needs-always-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-LINE-SUBSET-001","pythia","pythia-line-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["LS-NODE-SUBSET-001","ls","ls-node-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-WORKFLOW-DEFAULT-SHELL-001","cml","ci-workflow-default-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-DEFAULT-SHELL-001","cml","ci-job-default-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SHELL-OVERRIDE-PASS-001","cml","ci-step-shell-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-WORKFLOW-WORKDIR-001","cml","ci-workflow-workdir-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-WORKDIR-001","cml","ci-job-workdir-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-WORKDIR-OVERRIDE-PASS-001","cml","ci-step-workdir-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["IDENTITY-INVALID-001","cml","invalid_commit",["input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","COMMIT_SHA_INVALID"],["REF-MISSING-001","cml","blank_ref",["input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","REPOSITORY_REF_MISSING"],["SNAPSHOT-MISSING-001","cml","missing_snapshot",["input.snapshot_missing","outcome.unknown"],"audit","UNKNOWN","SNAPSHOT_UNAVAILABLE"],["CONTRACT-DRIFT-001","cml","missing_term",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_missing","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["IDENTITY-LIMIT-001","cml","valid_identity",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","risk.identity_overclaim","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["MANIFEST-INVALID-001","cml","invalid_manifest",["center.invariant_set","control.manifest_invalid","outcome.unknown"],"audit","UNKNOWN","MANIFEST_INVALID"],["MERGE-ELIGIBLE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_fresh","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"merge","MODEL","MODEL_ONLY"],["MERGE-STALE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_stale","state.merge_blocked"],"merge","MODEL","MODEL_ONLY"]]} diff --git a/standards/lotus-family/conformance/lotus_family_runtime.py b/standards/lotus-family/conformance/lotus_family_runtime.py index e0fd669c..7aeb5b5f 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime.py +++ b/standards/lotus-family/conformance/lotus_family_runtime.py @@ -1,3 +1,5 @@ +"""Lotus Family snapshot audit runtime.""" + from __future__ import annotations import argparse @@ -28,62 +30,133 @@ def audit_repository( repository_ref: str, commit_sha: str, ) -> dict[str, Any]: + """Audit one materialized snapshot under caller-claimed identity values.""" schema = str(manifest.get("schema_version", "")) try: validate_manifest(manifest) except (TypeError, ValueError) as exc: - return manifest_invalid(str(exc), repository_id, repository_ref, commit_sha, schema) + return manifest_invalid( + str(exc), repository_id, repository_ref, commit_sha, schema + ) + config = repository_config(manifest, repository_id) if config is None: - return result(outcome=UNKNOWN, reason_code="REPOSITORY_NOT_CONFIGURED", - detail=f"repository adapter is not configured: {repository_id}", - repository="", repository_id=repository_id, repository_ref=repository_ref, - commit_sha=commit_sha, manifest_schema=schema, checks=[], files=[]) + return result( + outcome=UNKNOWN, + reason_code="REPOSITORY_NOT_CONFIGURED", + detail=f"repository adapter is not configured: {repository_id}", + repository="", + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=schema, + checks=[], + files=[], + ) + repository = str(config["repository"]) - if not repository_ref: - return result(outcome=UNKNOWN, reason_code="REPOSITORY_REF_MISSING", - detail="an exact repository ref claim is required", repository=repository, - repository_id=repository_id, repository_ref=repository_ref, commit_sha=commit_sha, - manifest_schema=schema, checks=[], files=[]) + if not repository_ref or not repository_ref.strip(): + return result( + outcome=UNKNOWN, + reason_code="REPOSITORY_REF_MISSING", + detail="an exact repository ref claim is required", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=schema, + checks=[], + files=[], + ) if not COMMIT_SHA.fullmatch(commit_sha): - return result(outcome=UNKNOWN, reason_code="COMMIT_SHA_INVALID", - detail="commit_sha claim must be a lowercase 40-character hexadecimal SHA", - repository=repository, repository_id=repository_id, repository_ref=repository_ref, - commit_sha=commit_sha, manifest_schema=schema, checks=[], files=[]) + return result( + outcome=UNKNOWN, + reason_code="COMMIT_SHA_INVALID", + detail=( + "commit_sha claim must be a lowercase " + "40-character hexadecimal SHA" + ), + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=schema, + checks=[], + files=[], + ) + root = snapshot_root.resolve() repository_root = (root / str(config["snapshot_dir"])).resolve() try: repository_root.relative_to(root) except ValueError: - return result(outcome=UNKNOWN, reason_code="SNAPSHOT_UNAVAILABLE", - detail="repository snapshot escapes snapshot_root", repository=repository, - repository_id=repository_id, repository_ref=repository_ref, commit_sha=commit_sha, - manifest_schema=schema, checks=[], files=[]) + return result( + outcome=UNKNOWN, + reason_code="SNAPSHOT_UNAVAILABLE", + detail="repository snapshot escapes snapshot_root", + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=schema, + checks=[], + files=[], + ) if not repository_root.is_dir(): - return result(outcome=UNKNOWN, reason_code="SNAPSHOT_UNAVAILABLE", - detail=f"repository snapshot is unavailable: {config['snapshot_dir']}", - repository=repository, repository_id=repository_id, repository_ref=repository_ref, - commit_sha=commit_sha, manifest_schema=schema, checks=[], files=[]) + return result( + outcome=UNKNOWN, + reason_code="SNAPSHOT_UNAVAILABLE", + detail=( + "repository snapshot is unavailable: " + f"{config['snapshot_dir']}" + ), + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=schema, + checks=[], + files=[], + ) checks: list[dict[str, Any]] = [] files: list[dict[str, str]] = [] drift: list[str] = [] + for check in config["file_checks"]: check_id = str(check["id"]) name = str(check["path"]) text, error = read_file(repository_root, name, files) if error or text is None: - checks.append({"check_id": check_id, "outcome": DRIFT, "path": name, - "missing_terms": [], "detail": error}) + checks.append( + { + "check_id": check_id, + "outcome": DRIFT, + "path": name, + "missing_terms": [], + "detail": error, + } + ) drift.append(check_id) continue + terms = [str(term) for term in check["contains_all"]] missing = [term for term in terms if term not in text] - outcome = PASS if not missing else DRIFT - checks.append({"check_id": check_id, "outcome": outcome, "path": name, - "missing_terms": missing, - "detail": "all required terms are present" if outcome == PASS else "required terms are missing"}) - if outcome == DRIFT: + check_outcome = PASS if not missing else DRIFT + checks.append( + { + "check_id": check_id, + "outcome": check_outcome, + "path": name, + "missing_terms": missing, + "detail": ( + "all required terms are present" + if check_outcome == PASS + else "required terms are missing" + ), + } + ) + if check_outcome == DRIFT: drift.append(check_id) discovery = config["ci_discovery"] @@ -96,32 +169,66 @@ def audit_repository( errors.append(error or f"cannot read {name}") else: texts.append(text) + discovered, matched = ci_discovery(discovery, texts) if errors and not texts: - ci_outcome, detail = DRIFT, "; ".join(errors) + ci_outcome, ci_detail = DRIFT, "; ".join(errors) elif discovered: - ci_outcome, detail = PASS, "contract regression test is discovered by executable CI" + ci_outcome = PASS + ci_detail = ( + "contract regression test is discovered by executable CI" + ) else: - ci_outcome, detail = DRIFT, "no configured executable CI discovery rule matched" + ci_outcome = DRIFT + ci_detail = "no configured executable CI discovery rule matched" + if ci_outcome == DRIFT: drift.append("ci_discovery") - checks.append({"check_id": "ci_discovery", "outcome": ci_outcome, - "paths": workflow_paths, "matched_patterns": matched, "detail": detail}) + checks.append( + { + "check_id": "ci_discovery", + "outcome": ci_outcome, + "paths": workflow_paths, + "matched_patterns": matched, + "detail": ci_detail, + } + ) if drift: - outcome, reason = DRIFT, "LOTUS_CONTRACT_DRIFT" - detail = "non-conforming checks: " + ", ".join(sorted(set(drift))) + audit_outcome, reason = DRIFT, "LOTUS_CONTRACT_DRIFT" + detail = "non-conforming checks: " + ", ".join( + sorted(set(drift)) + ) else: - outcome, reason = PASS, "LOTUS_CONTRACT_CONFORMANT" - detail = "all configured Lotus Family invariants passed for the supplied snapshot and caller-provided identity claims" + audit_outcome, reason = PASS, "LOTUS_CONTRACT_CONFORMANT" + detail = ( + "all configured Lotus Family invariants passed for the " + "supplied snapshot and caller-provided identity claims" + ) + unique = {(row["path"], row["sha256"]): row for row in files} - return result(outcome=outcome, reason_code=reason, detail=detail, repository=repository, - repository_id=repository_id, repository_ref=repository_ref, commit_sha=commit_sha, - manifest_schema=schema, checks=checks, files=list(unique.values())) + return result( + outcome=audit_outcome, + reason_code=reason, + detail=detail, + repository=repository, + repository_id=repository_id, + repository_ref=repository_ref, + commit_sha=commit_sha, + manifest_schema=schema, + checks=checks, + files=list(unique.values()), + ) def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Audit one repository snapshot against the Lotus Family manifest.") + """Run the command-line auditor and write one JSON evidence artifact.""" + parser = argparse.ArgumentParser( + description=( + "Audit one repository snapshot against " + "the Lotus Family manifest." + ) + ) parser.add_argument("--manifest", type=Path, required=True) parser.add_argument("--snapshot-root", type=Path, required=True) parser.add_argument("--repository-id", required=True) @@ -129,13 +236,38 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--commit-sha", required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args(argv) + try: - audit = audit_repository(load_manifest(args.manifest), repository_id=args.repository_id, - snapshot_root=args.snapshot_root, repository_ref=args.repository_ref, - commit_sha=args.commit_sha) - except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: - audit = manifest_invalid(str(exc), args.repository_id, args.repository_ref, args.commit_sha, "") + audit = audit_repository( + load_manifest(args.manifest), + repository_id=args.repository_id, + snapshot_root=args.snapshot_root, + repository_ref=args.repository_ref, + commit_sha=args.commit_sha, + ) + except ( + OSError, + UnicodeError, + json.JSONDecodeError, + ValueError, + ) as exc: + audit = manifest_invalid( + str(exc), + args.repository_id, + args.repository_ref, + args.commit_sha, + "", + ) + args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(audit, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + args.output.write_text( + json.dumps( + audit, ensure_ascii=False, indent=2, sort_keys=True + ) + + "\n", + encoding="utf-8", + ) print(json.dumps(audit, ensure_ascii=False, sort_keys=True)) - return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(audit["outcome"], 3) + return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get( + audit["outcome"], 3 + ) diff --git a/standards/lotus-family/conformance/lotus_family_system_model.py b/standards/lotus-family/conformance/lotus_family_system_model.py index 9504097a..154b31c9 100644 --- a/standards/lotus-family/conformance/lotus_family_system_model.py +++ b/standards/lotus-family/conformance/lotus_family_system_model.py @@ -1,3 +1,5 @@ +"""Validation and derived views for the Lotus system graph.""" + from __future__ import annotations import json @@ -5,19 +7,24 @@ from pathlib import Path from typing import Any +GRAPH_ID = "lotus-family-system-v0.1.json" NODE_COLS = ["id", "type", "space", "time", "hierarchy", "centrality_role"] EDGE_COLS = ["id", "source", "target", "relation", "dimension"] ROUTE_COLS = ["id", "repo", "scenario", "path", "trajectory", "outcome", "reason"] def load(path: Path) -> dict[str, Any]: + """Load one compact graph or route model object.""" value = json.loads(path.read_text(encoding="utf-8")) if not isinstance(value, dict): raise ValueError("model root must be an object") return value -def rows(model: dict[str, Any], key: str, columns: list[str]) -> list[dict[str, Any]]: +def rows( + model: dict[str, Any], key: str, columns: list[str] +) -> list[dict[str, Any]]: + """Expand compact positional rows after checking their declared columns.""" declared = model.get(f"{key[:-1]}_columns") if key.endswith("s") else None if declared != columns: raise ValueError(f"unexpected {key} columns") @@ -30,19 +37,30 @@ def rows(model: dict[str, Any], key: str, columns: list[str]) -> list[dict[str, def validate_graph(graph: dict[str, Any]) -> dict[str, Any]: + """Validate graph identity, topology, dimensions, centers, and trajectories.""" if graph.get("schema_version") != "pythia.lotus_system_graph.compact.v0.1": raise ValueError("unsupported system graph schema") + if graph.get("graph_id") != GRAPH_ID: + raise ValueError("unexpected system graph identifier") if graph.get("authority") != "audit_only": raise ValueError("system graph must remain audit_only") - required_dimensions = {"causal", "spatial", "temporal", "hierarchy", "trajectory"} + required_dimensions = { + "causal", + "spatial", + "temporal", + "hierarchy", + "trajectory", + } if set(graph.get("dimensions", [])) != required_dimensions: raise ValueError("all five graph dimensions are required") + nodes = rows(graph, "nodes", NODE_COLS) edges = rows(graph, "edges", EDGE_COLS) node_ids = [node["id"] for node in nodes] edge_ids = [edge["id"] for edge in edges] if len(node_ids) != len(set(node_ids)) or len(edge_ids) != len(set(edge_ids)): raise ValueError("node and edge ids must be unique") + known = set(node_ids) phases = graph.get("temporal_phases", []) phase_index = {phase: index for index, phase in enumerate(phases)} @@ -51,8 +69,9 @@ def validate_graph(graph: dict[str, Any]) -> dict[str, Any]: raise ValueError(f"node lacks spacetime hierarchy: {node['id']}") if node["time"] not in phase_index and node["time"] != "anchor": raise ValueError(f"unknown temporal phase: {node['id']}") + adjacency: dict[str, list[str]] = {} - edge_pairs = set() + edge_pairs: set[tuple[str, str]] = set() for edge in edges: if edge["source"] not in known or edge["target"] not in known: raise ValueError(f"dangling edge: {edge['id']}") @@ -61,10 +80,12 @@ def validate_graph(graph: dict[str, Any]) -> dict[str, Any]: raise ValueError(f"duplicate edge pair: {pair}") edge_pairs.add(pair) adjacency.setdefault(edge["source"], []).append(edge["target"]) + visiting: set[str] = set() visited: set[str] = set() def visit(node: str) -> None: + """Depth-first cycle check.""" if node in visiting: raise ValueError(f"cycle at {node}") if node in visited: @@ -77,10 +98,12 @@ def visit(node: str) -> None: for node in known: visit(node) + centers = graph.get("centers", []) center_ids = {row[0] for row in centers} if not center_ids or not center_ids <= known: raise ValueError("centers must reference graph nodes") + trajectories = graph.get("trajectories", []) for trajectory_id, path, _ in trajectories: if len(path) < 2 or any(node not in known for node in path): @@ -88,12 +111,28 @@ def visit(node: str) -> None: for pair in zip(path, path[1:]): if pair not in edge_pairs: raise ValueError(f"trajectory gap {trajectory_id}: {pair}") - return {"nodes": nodes, "edges": edges, "edge_pairs": edge_pairs, "centers": center_ids} + return { + "nodes": nodes, + "edges": edges, + "edge_pairs": edge_pairs, + "centers": center_ids, + } -def validate_routes(graph: dict[str, Any], route_model: dict[str, Any]) -> list[dict[str, Any]]: - if route_model.get("schema_version") != "pythia.lotus_system_routes.compact.v0.1": + +def validate_routes( + graph: dict[str, Any], route_model: dict[str, Any] +) -> list[dict[str, Any]]: + """Validate route binding and every connected route path.""" + if ( + route_model.get("schema_version") + != "pythia.lotus_system_routes.compact.v0.1" + ): raise ValueError("unsupported route schema") + expected_graph = graph.get("graph_id") + if expected_graph != GRAPH_ID or route_model.get("graph") != expected_graph: + raise ValueError("route model is bound to a different system graph") + view = validate_graph(graph) routes = rows(route_model, "routes", ROUTE_COLS) route_ids: set[str] = set() @@ -110,33 +149,46 @@ def validate_routes(graph: dict[str, Any], route_model: dict[str, Any]) -> list[ return routes -def centrality_report(graph: dict[str, Any], routes: list[dict[str, Any]]) -> dict[str, Any]: +def centrality_report( + graph: dict[str, Any], routes: list[dict[str, Any]] +) -> dict[str, Any]: + """Compute review-priority centrality without assigning authority.""" view = validate_graph(graph) - degree = Counter() + degree: Counter[str] = Counter() for edge in view["edges"]: degree[edge["source"]] += 1 degree[edge["target"]] += 1 - route_hits = Counter(node for route in routes for node in route["path"]) + route_hits: Counter[str] = Counter( + node for route in routes for node in route["path"] + ) maximum = max(route_hits.values(), default=1) nodes = [] for node in view["nodes"]: node_id = node["id"] - nodes.append({ - "id": node_id, - "degree": degree[node_id], - "route_hits": route_hits[node_id], - "blast_radius": round(route_hits[node_id] / maximum, 3), - "is_center": node_id in view["centers"], - }) - nodes.sort(key=lambda row: (-row["route_hits"], -row["degree"], row["id"])) + nodes.append( + { + "id": node_id, + "degree": degree[node_id], + "route_hits": route_hits[node_id], + "blast_radius": round(route_hits[node_id] / maximum, 3), + "is_center": node_id in view["centers"], + } + ) + nodes.sort( + key=lambda row: (-row["route_hits"], -row["degree"], row["id"]) + ) return { "schema_version": "pythia.lotus_centrality_report.v0.1", - "meaning": "review priority and blast radius only; never ownership or authority", + "meaning": ( + "review priority and blast radius only; " + "never ownership or authority" + ), "nodes": nodes, } def traceability(routes: list[dict[str, Any]]) -> str: + """Render a human-readable ledger derived from executable routes.""" lines = [ "# Lotus system route ledger", "", @@ -145,7 +197,9 @@ def traceability(routes: list[dict[str, Any]]) -> str: ] for route in routes: lines.append( - f"| `{route['id']}` | `{route['repo']}` | `{route['scenario']}` | " - f"`{route['trajectory'] or 'runtime'}` | `{route['outcome']} / {route['reason']}` |" + f"| `{route['id']}` | `{route['repo']}` | " + f"`{route['scenario']}` | " + f"`{route['trajectory'] or 'runtime'}` | " + f"`{route['outcome']} / {route['reason']}` |" ) return "\n".join(lines) + "\n" diff --git a/standards/lotus-family/conformance/lotus_family_workflow_hardened.py b/standards/lotus-family/conformance/lotus_family_workflow_hardened.py index 0cb76f8f..a742d69a 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_hardened.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_hardened.py @@ -1,3 +1,5 @@ +"""Fail-closed GitHub Actions discovery for Lotus regression tests.""" + from __future__ import annotations import re @@ -11,9 +13,11 @@ _CONTROL_WORDS = legacy.CONTROL_WORDS _TERMINATORS = {"exit", "exec", "return", "break", "continue"} _POSIX_SHELLS = {"bash", "sh"} +_REPO_ROOT_EXPRESSIONS = {"${{ github.workspace }}"} def _condition(value: str) -> str | None: + """Normalize a static GitHub Actions condition.""" scalar = legacy.inline_scalar(value) if scalar is None: return None @@ -24,6 +28,7 @@ def _condition(value: str) -> str | None: def _supported_shell(value: str) -> bool: + """Accept only explicit POSIX shells whose script placeholder is executable.""" scalar = legacy.inline_scalar(value) if scalar is None: return False @@ -33,10 +38,13 @@ def _supported_shell(value: str) -> bool: return False if not parts or PurePosixPath(parts[0]).name not in _POSIX_SHELLS: return False - return len(parts) == 1 or (parts[-1] == "{0}" and all(p.startswith("-") for p in parts[1:-1])) + return len(parts) == 1 or ( + parts[-1] == "{0}" and all(part.startswith("-") for part in parts[1:-1]) + ) def _parse_needs(value: str) -> list[str] | None: + """Parse a static scalar or inline-list needs declaration.""" scalar = legacy.inline_scalar(value) if scalar is None: return None @@ -51,84 +59,200 @@ def _parse_needs(value: str) -> list[str] | None: return [key] if key else None +def _properties( + lines: list[str], + start: int, + end: int, + parent_indent: int, + scalar_body: set[int], +) -> dict[str, tuple[int, tuple[int, bool, int, str, str]]]: + """Return direct mapping children keyed by their decoded YAML key.""" + return { + header[3]: (row, header) + for row, header in legacy.direct_headers( + lines, start, end, parent_indent, scalar_body + ) + } + + +def _mapping_children( + lines: list[str], + entry: tuple[int, tuple[int, bool, int, str, str]], + scalar_body: set[int], +) -> dict[str, tuple[int, tuple[int, bool, int, str, str]]] | None: + """Read direct children of a mapping entry, rejecting scalar substitutions.""" + row, header = entry + if ( + legacy.inline_scalar(header[4]) is not None + or legacy.scalar_indicator(header[4]) is not None + ): + return None + end = legacy.block_end(lines, row, header[2]) + return _properties(lines, row + 1, end, header[2], scalar_body) + + +def _run_defaults( + lines: list[str], + properties: dict[str, tuple[int, tuple[int, bool, int, str, str]]], + scalar_body: set[int], +) -> dict[str, tuple[int, tuple[int, bool, int, str, str]]] | None: + """Resolve the static defaults.run mapping for one workflow or job scope.""" + defaults = properties.get("defaults") + if defaults is None: + return {} + default_properties = _mapping_children(lines, defaults, scalar_body) + if default_properties is None: + return None + run = default_properties.get("run") + if run is None: + return {} + return _mapping_children(lines, run, scalar_body) + + +def _effective_entry( + step_properties: dict[str, tuple[int, tuple[int, bool, int, str, str]]], + job_defaults: dict[str, tuple[int, tuple[int, bool, int, str, str]]], + workflow_defaults: dict[str, tuple[int, tuple[int, bool, int, str, str]]], + key: str, +) -> tuple[int, tuple[int, bool, int, str, str]] | None: + """Apply step → job defaults.run → workflow defaults.run inheritance.""" + if key in step_properties: + return step_properties[key] + if key in job_defaults: + return job_defaults[key] + return workflow_defaults.get(key) + + +def _repo_root_working_directory( + entry: tuple[int, tuple[int, bool, int, str, str]] | None, +) -> bool: + """Prove that a run step executes at repository root. + + Non-root or dynamic working directories are rejected because relative pytest + and Mix paths would otherwise refer to a different test scope. + """ + if entry is None: + return True + scalar = legacy.inline_scalar(entry[1][4]) + if scalar is None: + return False + text = scalar.strip().replace("\\", "/") + while text.startswith("./"): + text = text[2:] + return text in {"", "."} or scalar.strip() in _REPO_ROOT_EXPRESSIONS + + def github_run_scripts(text: str) -> list[str]: + """Extract only provably executable, repo-root POSIX run scripts.""" lines = text.splitlines() ranges = legacy.scalar_ranges(lines) scalar_body = { - row for start, (end, _) in ranges.items() for row in range(start + 1, end) + row + for start, (end, _) in ranges.items() + for row in range(start + 1, end) } + top = _properties(lines, 0, len(lines), -1, scalar_body) + jobs = top.get("jobs") + if jobs is None: + return [] + workflow_defaults = _run_defaults(lines, top, scalar_body) + if workflow_defaults is None: + return [] + jobs_children = _mapping_children(lines, jobs, scalar_body) + if jobs_children is None: + return [] + + job_entries = list(jobs_children.values()) + job_names = set(jobs_children) scripts: list[str] = [] - jobs = [ - (row, header) - for row, line in enumerate(lines) - if row not in scalar_body - and (header := legacy.yaml_header(line)) - and not header[1] - and header[2] == 0 - and header[3] == "jobs" - and legacy.inline_scalar(header[4]) is None - and legacy.scalar_indicator(header[4]) is None - ] - for jobs_row, jobs_header in jobs: - jobs_end = legacy.block_end(lines, jobs_row, jobs_header[2]) - job_headers = legacy.direct_headers(lines, jobs_row + 1, jobs_end, jobs_header[2], scalar_body) - job_names = {header[3] for _, header in job_headers} - for index, (job_row, job_header) in enumerate(job_headers): - job_end = job_headers[index + 1][0] if index + 1 < len(job_headers) else jobs_end - props = { - header[3]: (row, header) - for row, header in legacy.direct_headers(lines, job_row + 1, job_end, job_header[2], scalar_body) - } - runs_on = props.get("runs-on") - if not runs_on or legacy.inline_scalar(runs_on[1][4]) is None: + + for job_row, job_header in job_entries: + job_end = legacy.block_end(lines, job_row, job_header[2]) + job_properties = _properties( + lines, job_row + 1, job_end, job_header[2], scalar_body + ) + runs_on = job_properties.get("runs-on") + if runs_on is None or legacy.inline_scalar(runs_on[1][4]) is None: + continue + + needs = job_properties.get("needs") + if needs is not None: + dependencies = _parse_needs(needs[1][4]) + if ( + dependencies is None + or not dependencies + or any(dependency not in job_names for dependency in dependencies) + ): continue - needs = props.get("needs") - if needs: - dependencies = _parse_needs(needs[1][4]) - if dependencies is None or not dependencies or any(dep not in job_names for dep in dependencies): - continue - job_if = props.get("if") - if not job_if or _condition(job_if[1][4]) != "always()": - continue - else: - job_if = props.get("if") - if job_if and _condition(job_if[1][4]) != "true": - continue - steps = props.get("steps") - if not steps or legacy.inline_scalar(steps[1][4]) is not None or legacy.scalar_indicator(steps[1][4]) is not None: + job_if = job_properties.get("if") + if job_if is None or _condition(job_if[1][4]) != "always()": continue - steps_end = legacy.block_end(lines, steps[0], steps[1][2]) - items = legacy.item_starts(lines, steps[0] + 1, steps_end, scalar_body) - for step_index, item_row in enumerate(items): - item_end = items[step_index + 1] if step_index + 1 < len(items) else steps_end - first = legacy.yaml_header(lines[item_row]) - if not first or not first[1]: - continue - step_props = {first[3]: (item_row, first)} - for row in range(item_row + 1, item_end): - if row in scalar_body: - continue - header = legacy.yaml_header(lines[row]) - if header and not header[1] and header[2] == first[2]: - step_props[header[3]] = (row, header) - step_if = step_props.get("if") - if step_if and _condition(step_if[1][4]) != "true": - continue - shell = step_props.get("shell") - if shell and not _supported_shell(shell[1][4]): - continue - run = step_props.get("run") - if not run: + else: + job_if = job_properties.get("if") + if job_if is not None and _condition(job_if[1][4]) != "true": + continue + + job_defaults = _run_defaults(lines, job_properties, scalar_body) + if job_defaults is None: + continue + + steps = job_properties.get("steps") + if steps is None: + continue + if ( + legacy.inline_scalar(steps[1][4]) is not None + or legacy.scalar_indicator(steps[1][4]) is not None + ): + continue + steps_end = legacy.block_end(lines, steps[0], steps[1][2]) + items = legacy.item_starts(lines, steps[0] + 1, steps_end, scalar_body) + + for step_index, item_row in enumerate(items): + item_end = items[step_index + 1] if step_index + 1 < len(items) else steps_end + first = legacy.yaml_header(lines[item_row]) + if first is None or not first[1]: + continue + step_properties = {first[3]: (item_row, first)} + for row in range(item_row + 1, item_end): + if row in scalar_body: continue - if run[0] in ranges: - end, style = ranges[run[0]] - scripts.append(legacy.scalar_text(lines, run[0], end, style)) - elif (value := legacy.inline_scalar(run[1][4])) is not None: - scripts.append(value) + header = legacy.yaml_header(lines[row]) + if header is not None and not header[1] and header[2] == first[2]: + step_properties[header[3]] = (row, header) + + step_if = step_properties.get("if") + if step_if is not None and _condition(step_if[1][4]) != "true": + continue + + run = step_properties.get("run") + if run is None: + continue + + shell = _effective_entry( + step_properties, job_defaults, workflow_defaults, "shell" + ) + if shell is not None and not _supported_shell(shell[1][4]): + continue + + working_directory = _effective_entry( + step_properties, + job_defaults, + workflow_defaults, + "working-directory", + ) + if not _repo_root_working_directory(working_directory): + continue + + if run[0] in ranges: + end, style = ranges[run[0]] + scripts.append(legacy.scalar_text(lines, run[0], end, style)) + elif (value := legacy.inline_scalar(run[1][4])) is not None: + scripts.append(value) return scripts def shell_commands(text: str) -> list[str]: + """Split straight-line scripts and reject control flow or terminators.""" stripped = "\n".join(legacy.strip_comment(line) for line in text.splitlines()) try: lexer = shlex.shlex(stripped, posix=True, punctuation_chars=";&|(){}") @@ -139,6 +263,7 @@ def shell_commands(text: str) -> list[str]: return [] if any(token in _CONTROL or token in _CONTROL_WORDS for token in script_tokens): return [] + commands: list[str] = [] lines = text.splitlines() index = 0 @@ -159,7 +284,10 @@ def shell_commands(text: str) -> list[str]: parsed = shlex.split(command) except ValueError: return [] - if parsed and (legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) or parsed[0] in _TERMINATORS): + if parsed and ( + legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) + or parsed[0] in _TERMINATORS + ): return [] commands.append(command) index += 1 @@ -167,6 +295,7 @@ def shell_commands(text: str) -> list[str]: def _tokens(command: str) -> list[str] | None: + """Tokenize one direct command under the fail-closed shell policy.""" try: lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") lexer.whitespace_split = True @@ -174,77 +303,112 @@ def _tokens(command: str) -> list[str] | None: result = list(lexer) except ValueError: return None - if not result or any(token in _CONTROL for token in result) or legacy.ENV_ASSIGNMENT.fullmatch(result[0]) or result[0] in _TERMINATORS: + if ( + not result + or any(token in _CONTROL for token in result) + or legacy.ENV_ASSIGNMENT.fullmatch(result[0]) + or result[0] in _TERMINATORS + ): return None return result def pytest_command(command: str, test_path: str, require_path: bool) -> bool: + """Validate full pytest discovery or an explicit full-file selection.""" parts = _tokens(command) if not parts: return False for start in legacy.PYTEST_STARTS: if tuple(parts[: len(start)]) == start: - arguments = parts[len(start):] - if any("::" in arg for arg in arguments if not arg.startswith("-")): + arguments = parts[len(start) :] + if any("::" in argument for argument in arguments if not argument.startswith("-")): return False return legacy.pytest_safe(arguments, test_path, require_path) return False def mix_command(command: str, test_path: str, require_path: bool = False) -> bool: + """Validate Mix discovery while rejecting file-line subsets.""" parts = _tokens(command) if not parts or parts[:2] != ["mix", "test"]: return False positive: list[str] = [] - for arg in parts[2:]: - if arg in legacy.SAFE_MIX_FLAGS or arg.startswith(legacy.SAFE_MIX_PREFIXES): + for argument in parts[2:]: + if argument in legacy.SAFE_MIX_FLAGS or argument.startswith( + legacy.SAFE_MIX_PREFIXES + ): continue - if arg.startswith("-") or ":" in arg: + if argument.startswith("-") or ":" in argument: return False - positive.append(legacy.normalize(arg)) + positive.append(legacy.normalize(argument)) expected = legacy.normalize(test_path) - return expected in positive if require_path else (not positive or positive == [expected]) + return expected in positive if require_path else ( + not positive or positive == [expected] + ) -def ci_discovery(discovery: Mapping[str, Any], workflow_texts: str | list[str]) -> tuple[bool, list[str]]: +def ci_discovery( + discovery: Mapping[str, Any], workflow_texts: str | list[str] +) -> tuple[bool, list[str]]: + """Match configured Lotus tests only inside hardened executable contexts.""" text = "\n".join(workflow_texts) if isinstance(workflow_texts, list) else workflow_texts scripts = github_run_scripts(text) - commands = [command for script in scripts for command in shell_commands(script)] + commands = [ + command for script in scripts for command in shell_commands(script) + ] strategy = discovery["strategy"] uses_pytest = strategy == "pytest_default_discovery" or any( - str(pattern).endswith(".py") for pattern in discovery.get("contains_any", []) + str(pattern).endswith(".py") + for pattern in discovery.get("contains_any", []) ) if uses_pytest and ( any(name.startswith("PYTEST_") for name in legacy.yaml_env_names(text)) or any( re.search( r"\bPYTEST_[A-Za-z0-9_]*\b", - "\n".join(legacy.strip_comment(line) for line in script.splitlines()), + "\n".join( + legacy.strip_comment(line) for line in script.splitlines() + ), ) for script in scripts ) ): return False, [] + if strategy == "pytest_default_discovery": - matched = any(pytest_command(command, str(discovery["test_path"]), False) for command in commands) + matched = any( + pytest_command(command, str(discovery["test_path"]), False) + for command in commands + ) return matched, [str(discovery["command"])] if matched else [] if strategy == "mix_default_discovery": - matched = any(mix_command(command, str(discovery["test_path"])) for command in commands) + matched = any( + mix_command(command, str(discovery["test_path"])) + for command in commands + ) return matched, [str(discovery["command"])] if matched else [] + matches: list[str] = [] for value in discovery["contains_any"]: pattern = str(value) - if pattern.endswith(".py") and any(pytest_command(command, pattern, True) for command in commands): + if pattern.endswith(".py") and any( + pytest_command(command, pattern, True) for command in commands + ): matches.append(pattern) - elif pattern.endswith(".exs") and any(mix_command(command, pattern, True) for command in commands): + elif pattern.endswith(".exs") and any( + mix_command(command, pattern, True) for command in commands + ): matches.append(pattern) elif not pattern.endswith((".py", ".exs")): try: required = shlex.split(pattern) except ValueError: continue - if any((parts := _tokens(command)) and parts[: len(required)] == required for command in commands): + if any( + (parts := _tokens(command)) + and parts[: len(required)] == required + for command in commands + ): matches.append(pattern) return bool(matches), matches diff --git a/standards/lotus-family/conformance/test_causality_model.py b/standards/lotus-family/conformance/test_causality_model.py index 67adc711..45283d42 100644 --- a/standards/lotus-family/conformance/test_causality_model.py +++ b/standards/lotus-family/conformance/test_causality_model.py @@ -1,3 +1,5 @@ +"""Executable checks for the original Lotus causal route model.""" + from __future__ import annotations import copy @@ -6,25 +8,45 @@ import unittest from pathlib import Path -from lotus_family_auditor import DRIFT, PASS, UNKNOWN, audit_repository, load_manifest +from lotus_family_auditor import ( + DRIFT, + PASS, + UNKNOWN, + audit_repository, + load_manifest, +) HERE = Path(__file__).resolve().parent ROOT = HERE.parent MANIFEST_PATH = ROOT / "manifest" / "lotus-family-v0.1.json" -GRAPH_PATH = ROOT / "causality" / "lotus-family-causality-v0.1.json" +GRAPH_PATH = ( + ROOT / "causality" / "lotus-family-causality-v0.1.json" +) ROUTES_PATH = ROOT / "causality" / "test-paths-v0.1.json" TRACEABILITY_PATH = ROOT / "causality" / "TRACEABILITY.md" SHA = "a" * 40 +AUTHORITY_GRANTS = ( + "grants_ownership", + "grants_approval", + "grants_execution", + "grants_delivery", + "grants_merge", +) -def _write(root: Path, relative_path: str, content: str) -> None: +def _write( + root: Path, relative_path: str, content: str +) -> None: path = root / relative_path path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") def _workflow(command: str) -> str: - body = "\n".join(f" {line}" if line else "" for line in command.splitlines()) + body = "\n".join( + f" {line}" if line else "" + for line in command.splitlines() + ) return ( "name: CI\n" "jobs:\n" @@ -56,18 +78,32 @@ def _valid_command(discovery: dict) -> str: return pattern -def _materialize(snapshot_root: Path, config: dict, workflow: str | None = None) -> Path: +def _materialize( + snapshot_root: Path, + config: dict, + workflow: str | None = None, +) -> Path: repository_root = snapshot_root / config["snapshot_dir"] terms_by_path: dict[str, list[str]] = {} for check in config["file_checks"]: - terms_by_path.setdefault(check["path"], []).extend(check["contains_all"]) + terms_by_path.setdefault(check["path"], []).extend( + check["contains_all"] + ) for relative_path, terms in terms_by_path.items(): - _write(repository_root, relative_path, "\n".join(dict.fromkeys(terms)) + "\n") + _write( + repository_root, + relative_path, + "\n".join(dict.fromkeys(terms)) + "\n", + ) discovery = config["ci_discovery"] _write( repository_root, discovery["workflow_paths"][0], - workflow if workflow is not None else _workflow(_valid_command(discovery)), + ( + workflow + if workflow is not None + else _workflow(_valid_command(discovery)) + ), ) return repository_root @@ -75,8 +111,12 @@ def _materialize(snapshot_root: Path, config: dict, workflow: str | None = None) class CausalityModelTest(unittest.TestCase): def setUp(self) -> None: self.manifest = load_manifest(MANIFEST_PATH) - self.graph = json.loads(GRAPH_PATH.read_text(encoding="utf-8")) - self.routes = json.loads(ROUTES_PATH.read_text(encoding="utf-8"))["routes"] + self.graph = json.loads( + GRAPH_PATH.read_text(encoding="utf-8") + ) + self.routes = json.loads( + ROUTES_PATH.read_text(encoding="utf-8") + )["routes"] def config(self, repository_id: str) -> dict: return next( @@ -85,9 +125,15 @@ def config(self, repository_id: str) -> dict: if row["id"] == repository_id ) - def test_graph_has_unique_nodes_edges_and_no_dangling_references(self): - node_ids = [node["id"] for node in self.graph["nodes"]] - edge_ids = [edge["id"] for edge in self.graph["edges"]] + def test_graph_has_unique_nodes_edges_and_no_dangling_references( + self, + ) -> None: + node_ids = [ + node["id"] for node in self.graph["nodes"] + ] + edge_ids = [ + edge["id"] for edge in self.graph["edges"] + ] self.assertEqual(len(node_ids), len(set(node_ids))) self.assertEqual(len(edge_ids), len(set(edge_ids))) known = set(node_ids) @@ -95,16 +141,21 @@ def test_graph_has_unique_nodes_edges_and_no_dangling_references(self): self.assertIn(edge["source"], known) self.assertIn(edge["target"], known) - def test_graph_is_acyclic(self): + def test_graph_is_acyclic(self) -> None: adjacency: dict[str, list[str]] = {} for edge in self.graph["edges"]: - adjacency.setdefault(edge["source"], []).append(edge["target"]) + adjacency.setdefault( + edge["source"], [] + ).append(edge["target"]) visiting: set[str] = set() visited: set[str] = set() def visit(node: str) -> None: if node in visiting: - self.fail(f"causality graph contains a cycle at {node}") + self.fail( + "causality graph contains a cycle " + f"at {node}" + ) if node in visited: return visiting.add(node) @@ -113,11 +164,18 @@ def visit(node: str) -> None: visiting.remove(node) visited.add(node) - for node in {row["id"] for row in self.graph["nodes"]}: + for node in { + row["id"] for row in self.graph["nodes"] + }: visit(node) - def test_every_route_is_a_connected_graph_path(self): - edges = {(edge["source"], edge["target"]) for edge in self.graph["edges"]} + def test_every_route_is_a_connected_graph_path( + self, + ) -> None: + edges = { + (edge["source"], edge["target"]) + for edge in self.graph["edges"] + } route_ids: set[str] = set() for route in self.routes: self.assertNotIn(route["id"], route_ids) @@ -125,30 +183,52 @@ def test_every_route_is_a_connected_graph_path(self): path = route["path"] self.assertGreaterEqual(len(path), 2) for source, target in zip(path, path[1:]): - self.assertIn((source, target), edges, route["id"]) + self.assertIn( + (source, target), edges, route["id"] + ) - def test_required_graph_nodes_are_covered_by_routes(self): - required_types = set(self.graph["coverage_policy"]["required_node_types"]) + def test_required_graph_nodes_are_covered_by_routes( + self, + ) -> None: + required_types = set( + self.graph["coverage_policy"][ + "required_node_types" + ] + ) required_nodes = { node["id"] for node in self.graph["nodes"] if node["type"] in required_types } - covered = {node for route in self.routes for node in route["path"]} + covered = { + node + for route in self.routes + for node in route["path"] + } self.assertEqual(required_nodes - covered, set()) - def test_traceability_names_every_executable_route(self): - traceability = TRACEABILITY_PATH.read_text(encoding="utf-8") + def test_traceability_names_every_executable_route( + self, + ) -> None: + traceability = TRACEABILITY_PATH.read_text( + encoding="utf-8" + ) for route in self.routes: - self.assertIn(f"`{route['id']}`", traceability) + self.assertIn( + f"`{route['id']}`", traceability + ) - def test_causal_routes_execute_to_their_expected_outcomes(self): + def test_causal_routes_execute_to_their_expected_outcomes( + self, + ) -> None: for route in self.routes: with self.subTest(route=route["id"]): scenario = route["scenario"] repository_id = route["repository_id"] manifest = copy.deepcopy(self.manifest) - commit_sha = scenario.get("commit_sha", SHA) + commit_sha = scenario.get( + "commit_sha", SHA + ) with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) if scenario["kind"] != "missing_snapshot": @@ -158,18 +238,30 @@ def test_causal_routes_execute_to_their_expected_outcomes(self): if row["id"] == repository_id ) workflow = scenario.get("workflow") - repository_root = _materialize(snapshot_root, config, workflow) + repository_root = _materialize( + snapshot_root, config, workflow + ) if scenario["kind"] == "missing_term": check = config["file_checks"][0] - path = repository_root / check["path"] + path = ( + repository_root / check["path"] + ) path.write_text( - path.read_text(encoding="utf-8").replace( - check["contains_all"][0], "" + path.read_text( + encoding="utf-8" + ).replace( + check["contains_all"][0], + "", ), encoding="utf-8", ) - elif scenario["kind"] == "invalid_manifest": - config["file_checks"][0]["contains_all"] = [] + elif ( + scenario["kind"] + == "invalid_manifest" + ): + config["file_checks"][0][ + "contains_all" + ] = [] result = audit_repository( manifest, @@ -178,16 +270,29 @@ def test_causal_routes_execute_to_their_expected_outcomes(self): repository_ref="refs/heads/main", commit_sha=commit_sha, ) - self.assertEqual(result["outcome"], route["expected"]["outcome"]) self.assertEqual( - result["reason_code"], route["expected"]["reason_code"] + result["outcome"], + route["expected"]["outcome"], + ) + self.assertEqual( + result["reason_code"], + route["expected"]["reason_code"], + ) + identity_mode = scenario.get( + "assert_identity_mode" ) - identity_mode = scenario.get("assert_identity_mode") if identity_mode is not None: self.assertEqual( - result["identity_assurance"]["mode"], identity_mode + result["identity_assurance"][ + "mode" + ], + identity_mode, + ) + for grant in AUTHORITY_GRANTS: + self.assertFalse( + result["authority"][grant], + f"{route['id']} granted {grant}", ) - self.assertFalse(result["authority"]["grants_merge"]) if __name__ == "__main__": diff --git a/standards/lotus-family/conformance/test_system_model.py b/standards/lotus-family/conformance/test_system_model.py index d3e2b4fc..e2c463a5 100644 --- a/standards/lotus-family/conformance/test_system_model.py +++ b/standards/lotus-family/conformance/test_system_model.py @@ -1,3 +1,5 @@ +"""Executable validation for the Lotus causal spacetime system model.""" + from __future__ import annotations import copy @@ -5,8 +7,20 @@ import unittest from pathlib import Path -from lotus_family_auditor import DRIFT, PASS, UNKNOWN, audit_repository, load_manifest -from lotus_family_system_model import centrality_report, load, traceability, validate_graph, validate_routes +from lotus_family_auditor import ( + DRIFT, + PASS, + UNKNOWN, + audit_repository, + load_manifest, +) +from lotus_family_system_model import ( + centrality_report, + load, + traceability, + validate_graph, + validate_routes, +) HERE = Path(__file__).resolve().parent ROOT = HERE.parent @@ -14,111 +28,408 @@ ROUTES = ROOT / "causality" / "system-routes-v0.1.json" MANIFEST = ROOT / "manifest" / "lotus-family-v0.1.json" SHA = "a" * 40 +AUTHORITY_GRANTS = ( + "grants_ownership", + "grants_approval", + "grants_execution", + "grants_delivery", + "grants_merge", +) -def workflow(command: str, *, job_extra: str = "", step_extra: str = "", prefix_jobs: str = "") -> str: - body = "\n".join(f" {line}" if line else "" for line in command.splitlines()) - return f"name: CI\njobs:\n{prefix_jobs} test:\n runs-on: ubuntu-latest\n{job_extra} steps:\n - name: Run tests\n{step_extra} run: |\n{body}\n" +def workflow( + command: str, + *, + workflow_extra: str = "", + job_extra: str = "", + step_extra: str = "", + prefix_jobs: str = "", +) -> str: + """Build a small GitHub Actions workflow fixture.""" + body = "\n".join( + f" {line}" if line else "" + for line in command.splitlines() + ) + return ( + "name: CI\n" + f"{workflow_extra}" + "jobs:\n" + f"{prefix_jobs}" + " test:\n" + " runs-on: ubuntu-latest\n" + f"{job_extra}" + " steps:\n" + " - name: Run tests\n" + f"{step_extra}" + " run: |\n" + f"{body}\n" + ) SCENARIOS = { - "ci-nonrun-001": "name: CI\nenv:\n NOTE: python -m pytest\njobs: {}\n", - "ci-step-skip-001": workflow("python -m pytest", step_extra=" if: ${{ false }}\n"), - "ci-job-skip-001": workflow("python -m pytest", job_extra=" if: ${{ false }}\n"), - "ci-no-runner-001": "name: CI\njobs:\n test:\n steps:\n - run: python -m pytest\n", - "ci-quoted-env-001": 'name: CI\nenv:\n "PYTEST_ADDOPTS": "--ignore=tests/test_lotus_docs_contract.py"\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: python -m pytest\n', - "ci-shell-control-001": workflow("if false; then\n python -m pytest\nfi"), - "cml-subset-001": workflow("python -m pytest tests/test_other.py"), - "pythia-subset-001": workflow("mix test test/unrelated_test.exs"), - "ci-fake-steps-001": "name: CI\nmetadata:\n fake:\n steps:\n - run: python -m pytest\njobs: {}\n", - "ci-custom-shell-001": workflow("python -m pytest", step_extra=" shell: cat {0}\n"), - "ci-terminator-exit-001": workflow("exit 0\npython -m pytest"), - "ci-needs-skip-001": workflow("python -m pytest", job_extra=" needs: gate\n", prefix_jobs=" gate:\n if: ${{ false }}\n runs-on: ubuntu-latest\n steps:\n - run: echo skipped\n"), - "ci-needs-always-pass-001": workflow("python -m pytest", job_extra=" needs: gate\n if: ${{ always() }}\n", prefix_jobs=" gate:\n if: ${{ false }}\n runs-on: ubuntu-latest\n steps:\n - run: echo skipped\n"), - "pythia-line-subset-001": workflow("mix test test/lotus_docs_contract_test.exs:12"), - "ls-node-subset-001": workflow("python -m pytest tests/test_lotus_docs_contract.py::test_pr_evidence_is_bound_to_exact_head"), + "ci-nonrun-001": ( + "name: CI\n" + "env:\n" + " NOTE: python -m pytest\n" + "jobs: {}\n" + ), + "ci-step-skip-001": workflow( + "python -m pytest", + step_extra=" if: ${{ false }}\n", + ), + "ci-job-skip-001": workflow( + "python -m pytest", + job_extra=" if: ${{ false }}\n", + ), + "ci-no-runner-001": ( + "name: CI\n" + "jobs:\n" + " test:\n" + " steps:\n" + " - run: python -m pytest\n" + ), + "ci-quoted-env-001": ( + 'name: CI\n' + 'env:\n' + ' "PYTEST_ADDOPTS": ' + '"--ignore=tests/test_lotus_docs_contract.py"\n' + 'jobs:\n' + ' test:\n' + ' runs-on: ubuntu-latest\n' + ' steps:\n' + ' - run: python -m pytest\n' + ), + "ci-shell-control-001": workflow( + "if false; then\n python -m pytest\nfi" + ), + "cml-subset-001": workflow( + "python -m pytest tests/test_other.py" + ), + "pythia-subset-001": workflow( + "mix test test/unrelated_test.exs" + ), + "ci-fake-steps-001": ( + "name: CI\n" + "metadata:\n" + " fake:\n" + " steps:\n" + " - run: python -m pytest\n" + "jobs: {}\n" + ), + "ci-custom-shell-001": workflow( + "python -m pytest", + step_extra=" shell: cat {0}\n", + ), + "ci-terminator-exit-001": workflow( + "exit 0\npython -m pytest" + ), + "ci-needs-skip-001": workflow( + "python -m pytest", + job_extra=" needs: gate\n", + prefix_jobs=( + " gate:\n" + " if: ${{ false }}\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - run: echo skipped\n" + ), + ), + "ci-needs-always-pass-001": workflow( + "python -m pytest", + job_extra=( + " needs: gate\n" + " if: ${{ always() }}\n" + ), + prefix_jobs=( + " gate:\n" + " if: ${{ false }}\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - run: echo skipped\n" + ), + ), + "pythia-line-subset-001": workflow( + "mix test test/lotus_docs_contract_test.exs:12" + ), + "ls-node-subset-001": workflow( + "python -m pytest " + "tests/test_lotus_docs_contract.py::" + "test_pr_evidence_is_bound_to_exact_head" + ), + "ci-workflow-default-shell-001": workflow( + "python -m pytest", + workflow_extra=( + "defaults:\n" + " run:\n" + " shell: cat {0}\n" + ), + ), + "ci-job-default-shell-001": workflow( + "python -m pytest", + job_extra=( + " defaults:\n" + " run:\n" + " shell: cat {0}\n" + ), + ), + "ci-step-shell-override-pass-001": workflow( + "python -m pytest", + workflow_extra=( + "defaults:\n" + " run:\n" + " shell: cat {0}\n" + ), + step_extra=" shell: bash\n", + ), + "ci-workflow-workdir-001": workflow( + "python -m pytest", + workflow_extra=( + "defaults:\n" + " run:\n" + " working-directory: subdir\n" + ), + ), + "ci-job-workdir-001": workflow( + "python -m pytest", + job_extra=( + " defaults:\n" + " run:\n" + " working-directory: subdir\n" + ), + ), + "ci-step-workdir-override-pass-001": workflow( + "python -m pytest", + workflow_extra=( + "defaults:\n" + " run:\n" + " working-directory: subdir\n" + ), + step_extra=" working-directory: .\n", + ), } def valid_command(discovery: dict) -> str: + """Return one valid command for a repository adapter.""" if discovery["strategy"] == "pytest_default_discovery": return "python -m pytest" if discovery["strategy"] == "mix_default_discovery": return "mix test" pattern = discovery["contains_any"][0] - return f"python -m pytest {pattern}" if pattern.endswith(".py") else f"mix test {pattern}" + return ( + f"python -m pytest {pattern}" + if pattern.endswith(".py") + else f"mix test {pattern}" + ) -def materialize(root: Path, config: dict, text: str | None = None) -> Path: - repo = root / config["snapshot_dir"] +def materialize( + root: Path, config: dict, text: str | None = None +) -> Path: + """Materialize the minimum valid snapshot required by the manifest.""" + repository = root / config["snapshot_dir"] terms: dict[str, list[str]] = {} for check in config["file_checks"]: - terms.setdefault(check["path"], []).extend(check["contains_all"]) + terms.setdefault(check["path"], []).extend( + check["contains_all"] + ) for name, values in terms.items(): - path = repo / name + path = repository / name path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("\n".join(dict.fromkeys(values)) + "\n", encoding="utf-8") + path.write_text( + "\n".join(dict.fromkeys(values)) + "\n", + encoding="utf-8", + ) discovery = config["ci_discovery"] - path = repo / discovery["workflow_paths"][0] + path = repository / discovery["workflow_paths"][0] path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(text or workflow(valid_command(discovery)), encoding="utf-8") - return repo + path.write_text( + text or workflow(valid_command(discovery)), + encoding="utf-8", + ) + return repository class SystemModelTest(unittest.TestCase): - def setUp(self): + """Validate graph integrity and execute every runtime route.""" + + def setUp(self) -> None: self.graph = load(GRAPH) self.route_model = load(ROUTES) - self.routes = validate_routes(self.graph, self.route_model) + self.routes = validate_routes( + self.graph, self.route_model + ) self.manifest = load_manifest(MANIFEST) - def test_graph_has_five_dimensions_centers_and_trajectories(self): + def test_graph_has_five_dimensions_centers_and_trajectories( + self, + ) -> None: view = validate_graph(self.graph) - self.assertEqual(len(view["nodes"]), 46) - self.assertEqual(len(view["edges"]), 62) - self.assertEqual(len(self.routes), 25) - self.assertEqual(set(self.graph["dimensions"]), {"causal", "spatial", "temporal", "hierarchy", "trajectory"}) + self.assertEqual(len(view["nodes"]), 49) + self.assertEqual(len(view["edges"]), 64) + self.assertEqual(len(self.routes), 32) + self.assertEqual( + set(self.graph["dimensions"]), + { + "causal", + "spatial", + "temporal", + "hierarchy", + "trajectory", + }, + ) + + def test_route_model_is_bound_to_exact_graph(self) -> None: + wrong = copy.deepcopy(self.route_model) + wrong["graph"] = "different-system-graph.json" + with self.assertRaisesRegex( + ValueError, "different system graph" + ): + validate_routes(self.graph, wrong) + + def test_runtime_pass_does_not_claim_exact_head_freshness( + self, + ) -> None: + runtime_passes = [ + route + for route in self.routes + if route["outcome"] == PASS + and route["trajectory"] == "audit" + ] + for route in runtime_passes: + self.assertNotIn( + "center.exact_head", route["path"], route["id"] + ) + self.assertNotIn( + "time.evidence_fresh", route["path"], route["id"] + ) + self.assertIn( + "limitation.identity_unverified", + route["path"], + route["id"], + ) - def test_centrality_is_review_priority_not_authority(self): + def test_centrality_is_review_priority_not_authority( + self, + ) -> None: report = centrality_report(self.graph, self.routes) - self.assertIn("never ownership or authority", report["meaning"]) - self.assertEqual(len(report["nodes"]), 46) + self.assertIn( + "never ownership or authority", report["meaning"] + ) + self.assertEqual(len(report["nodes"]), 49) - def test_traceability_is_derived_from_routes(self): + def test_traceability_is_derived_from_routes(self) -> None: ledger = traceability(self.routes) for route in self.routes: self.assertIn(f"`{route['id']}`", ledger) - def test_runtime_routes_reach_expected_outcomes(self): - runtime = [route for route in self.routes if route["outcome"] in {PASS, DRIFT, UNKNOWN}] + def test_runtime_routes_reach_expected_outcomes( + self, + ) -> None: + runtime = [ + route + for route in self.routes + if route["outcome"] in {PASS, DRIFT, UNKNOWN} + ] for route in runtime: with self.subTest(route=route["id"]): manifest = copy.deepcopy(self.manifest) - config = next(row for row in manifest["repositories"] if row["id"] == route["repo"]) + config = next( + row + for row in manifest["repositories"] + if row["id"] == route["repo"] + ) with tempfile.TemporaryDirectory() as directory: root = Path(directory) if route["scenario"] != "missing_snapshot": - repo = materialize(root, config, SCENARIOS.get(route["scenario"])) + repository = materialize( + root, + config, + SCENARIOS.get(route["scenario"]), + ) if route["scenario"] == "missing_term": check = config["file_checks"][0] - path = repo / check["path"] - path.write_text(path.read_text(encoding="utf-8").replace(check["contains_all"][0], ""), encoding="utf-8") - elif route["scenario"] == "invalid_manifest": - config["file_checks"][0]["contains_all"] = [] + path = repository / check["path"] + path.write_text( + path.read_text( + encoding="utf-8" + ).replace( + check["contains_all"][0], "" + ), + encoding="utf-8", + ) + elif ( + route["scenario"] + == "invalid_manifest" + ): + config["file_checks"][0][ + "contains_all" + ] = [] + + repository_ref = ( + " " + if route["scenario"] == "blank_ref" + else "refs/heads/main" + ) + commit_sha = ( + "main" + if route["scenario"] + == "invalid_commit" + else SHA + ) result = audit_repository( manifest, repository_id=route["repo"], snapshot_root=root, - repository_ref="refs/heads/main", - commit_sha="main" if route["scenario"] == "invalid_commit" else SHA, + repository_ref=repository_ref, + commit_sha=commit_sha, + ) + self.assertEqual( + ( + result["outcome"], + result["reason_code"], + ), + ( + route["outcome"], + route["reason"], + ), ) - self.assertEqual((result["outcome"], result["reason_code"]), (route["outcome"], route["reason"])) - self.assertFalse(result["authority"]["grants_merge"]) + for grant in AUTHORITY_GRANTS: + self.assertFalse( + result["authority"][grant], + f"{route['id']} granted {grant}", + ) - def test_merge_trajectory_never_grants_merge_authority(self): - model_routes = [route for route in self.routes if route["outcome"] == "MODEL"] - self.assertEqual({route["id"] for route in model_routes}, {"MERGE-ELIGIBLE-001", "MERGE-STALE-001"}) - self.assertTrue(all(route["path"][-1] in {"authority.advisory_only", "state.merge_blocked"} for route in model_routes)) + def test_merge_trajectory_never_grants_merge_authority( + self, + ) -> None: + model_routes = [ + route + for route in self.routes + if route["outcome"] == "MODEL" + ] + self.assertEqual( + {route["id"] for route in model_routes}, + {"MERGE-ELIGIBLE-001", "MERGE-STALE-001"}, + ) + eligible = next( + route + for route in model_routes + if route["id"] == "MERGE-ELIGIBLE-001" + ) + self.assertIn( + "gate.provenance_verified", eligible["path"] + ) + self.assertTrue( + all( + route["path"][-1] + in { + "authority.advisory_only", + "state.merge_blocked", + } + for route in model_routes + ) + ) if __name__ == "__main__": From 9df59425a825aafb543e018df30f9332d4049114 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:53:06 +0300 Subject: [PATCH 30/79] fix: reject non-executing shells and cyclic jobs --- .../lotus-family/causality/SYSTEM-MODEL.md | 2 +- .../causality/system-routes-v0.1.json | 2 +- .../conformance/lotus_family_workflow.py | 2 +- .../lotus_family_workflow_policy.py | 281 ++++++++++++++++++ .../conformance/test_system_model.py | 34 ++- 5 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 standards/lotus-family/conformance/lotus_family_workflow_policy.py diff --git a/standards/lotus-family/causality/SYSTEM-MODEL.md b/standards/lotus-family/causality/SYSTEM-MODEL.md index 4f616432..6d6fc9d0 100644 --- a/standards/lotus-family/causality/SYSTEM-MODEL.md +++ b/standards/lotus-family/causality/SYSTEM-MODEL.md @@ -20,7 +20,7 @@ Machine-readable sources: - `lotus-family-system-v0.1.json` — 49 nodes, 64 unique directed relationships, centers, and four canonical trajectories; -- `system-routes-v0.1.json` — 32 connected routes, including runtime +- `system-routes-v0.1.json` — 36 connected routes, including runtime regressions and governance-only merge trajectories. ## Centers without centralized authority diff --git a/standards/lotus-family/causality/system-routes-v0.1.json b/standards/lotus-family/causality/system-routes-v0.1.json index f884460e..6ded475b 100644 --- a/standards/lotus-family/causality/system-routes-v0.1.json +++ b/standards/lotus-family/causality/system-routes-v0.1.json @@ -1 +1 @@ -{"schema_version":"pythia.lotus_system_routes.compact.v0.1","graph":"lotus-family-system-v0.1.json","route_columns":["id","repo","scenario","path","trajectory","outcome","reason"],"routes":[["CML-PASS-001","cml","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["LS-PASS-001","ls","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-PASS-001","pythia","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-NONRUN-001","cml","ci-nonrun-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","control.non_run_text","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SKIP-001","cml","ci-step-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_skipped","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-SKIP-001","cml","ci-job-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NO-RUNNER-001","cml","ci-no-runner-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-QUOTED-ENV-001","cml","ci-quoted-env-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_override","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-CONTROL-001","cml","ci-shell-control-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_control_flow","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CML-SUBSET-001","cml","cml-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["PYTHIA-SUBSET-001","pythia","pythia-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-FAKE-STEPS-001","cml","ci-fake-steps-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-CUSTOM-SHELL-001","cml","ci-custom-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-TERMINATOR-EXIT-001","cml","ci-terminator-exit-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-SKIP-001","cml","ci-needs-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-ALWAYS-PASS-001","cml","ci-needs-always-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-LINE-SUBSET-001","pythia","pythia-line-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["LS-NODE-SUBSET-001","ls","ls-node-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-WORKFLOW-DEFAULT-SHELL-001","cml","ci-workflow-default-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-DEFAULT-SHELL-001","cml","ci-job-default-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SHELL-OVERRIDE-PASS-001","cml","ci-step-shell-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-WORKFLOW-WORKDIR-001","cml","ci-workflow-workdir-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-WORKDIR-001","cml","ci-job-workdir-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-WORKDIR-OVERRIDE-PASS-001","cml","ci-step-workdir-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["IDENTITY-INVALID-001","cml","invalid_commit",["input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","COMMIT_SHA_INVALID"],["REF-MISSING-001","cml","blank_ref",["input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","REPOSITORY_REF_MISSING"],["SNAPSHOT-MISSING-001","cml","missing_snapshot",["input.snapshot_missing","outcome.unknown"],"audit","UNKNOWN","SNAPSHOT_UNAVAILABLE"],["CONTRACT-DRIFT-001","cml","missing_term",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_missing","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["IDENTITY-LIMIT-001","cml","valid_identity",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","risk.identity_overclaim","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["MANIFEST-INVALID-001","cml","invalid_manifest",["center.invariant_set","control.manifest_invalid","outcome.unknown"],"audit","UNKNOWN","MANIFEST_INVALID"],["MERGE-ELIGIBLE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_fresh","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"merge","MODEL","MODEL_ONLY"],["MERGE-STALE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_stale","state.merge_blocked"],"merge","MODEL","MODEL_ONLY"]]} +{"schema_version":"pythia.lotus_system_routes.compact.v0.1","graph":"lotus-family-system-v0.1.json","route_columns":["id","repo","scenario","path","trajectory","outcome","reason"],"routes":[["CML-PASS-001","cml","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["LS-PASS-001","ls","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-PASS-001","pythia","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-NONRUN-001","cml","ci-nonrun-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","control.non_run_text","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SKIP-001","cml","ci-step-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_skipped","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-SKIP-001","cml","ci-job-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NO-RUNNER-001","cml","ci-no-runner-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-QUOTED-ENV-001","cml","ci-quoted-env-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_override","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-CONTROL-001","cml","ci-shell-control-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_control_flow","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CML-SUBSET-001","cml","cml-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["PYTHIA-SUBSET-001","pythia","pythia-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-FAKE-STEPS-001","cml","ci-fake-steps-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-CUSTOM-SHELL-001","cml","ci-custom-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-TERMINATOR-EXIT-001","cml","ci-terminator-exit-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-SKIP-001","cml","ci-needs-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-ALWAYS-PASS-001","cml","ci-needs-always-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-LINE-SUBSET-001","pythia","pythia-line-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["LS-NODE-SUBSET-001","ls","ls-node-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-WORKFLOW-DEFAULT-SHELL-001","cml","ci-workflow-default-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-DEFAULT-SHELL-001","cml","ci-job-default-shell-001",["input.snapshot_present","input.sdentity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SHELL-OVERRIDE-PASS-001","cml","ci-step-shell-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-WORKFLOW-WORKDIR-001","cml","ci-workflow-workdir-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-WORKDIR-001","cml","ci-job-workdir-001",["input.snapshot_present","input.sdentity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-WORKDIR-OVERRIDE-PASS-001","cml","ci-step-workdir-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["IDENTITY-INVALID-001","cml","invalid_commit",["input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","COMMIT_SHA_INVALID"],["REF-MISSING-001","cml","blank_ref",["input.snapshot_present","input.sdentity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","REPOSITORY_REF_MISSING"],["SNAPSHOT-MISSING-001","cml","missing_snapshot",["input.snapshot_missing","outcome.unknown"],"audit","UNKNOWN","SNAPSHOT_UNAVAILABLE"],["CONTRACT-DRIFT-001","cml","missing_term",["input.snapshot_present","input.sdentity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_missing","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["IDENTITY-LIMIT-001","cml","valid_identity",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","risk.identity_overclaim","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["MANIFEST-INVALID-001","cml","invalid_manifest",["center.invariant_set","control.manifest_invalid","outcome.unknown"],"audit","UNKNOWN","MANIFEST_INVALID"],["MERGE-ELIGIBLE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_fresh","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"merge","MODEL","MODEL_ONLY"],["MERGE-STALE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_stale","state.merge_blocked"],"merge","MODEL","MODEL_ONLY"],["CI-SHELL-NOEXEC-001","cml","ci-shell-noexec-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-VERSION-001","cml","ci-shell-version-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-WRAPPED-TERMINATOR-001","cml","ci-wrapped-terminator-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-CYCLE-001","cml","ci-needs-cycle-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"]]} diff --git a/standards/lotus-family/conformance/lotus_family_workflow.py b/standards/lotus-family/conformance/lotus_family_workflow.py index a650703e..6b874c8b 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow.py +++ b/standards/lotus-family/conformance/lotus_family_workflow.py @@ -1,3 +1,3 @@ """Public fail-closed GitHub Actions discovery API.""" -from lotus_family_workflow_hardened import * # noqa: F403 +from lotus_family_workflow_policy import * # noqa: F403 diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy.py b/standards/lotus-family/conformance/lotus_family_workflow_policy.py new file mode 100644 index 00000000..50b72b4f --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy.py @@ -0,0 +1,281 @@ +"""Final fail-closed execution policy for Lotus GitHub Actions discovery.""" + +from __future__ import annotations + +import re +import shlex +from pathlib import PurePosixPath +from typing import Any, Mapping + +import lotus_family_workflow_hardened as base +import lotus_family_workflow_legacy as legacy + +_POSIX_SHELLS = {"bash", "sh"} +_SHELL_WRAPPERS = {"command", "builtin", "eval"} +_TERMINATORS = {"exit", "exec", "return", "break", "continue"} +_CONTROL = legacy.CONTROL_TOKENS | {"(", ")", "{", "}"} + + +def _supported_shell(value: str) -> bool: + """Accept only shell forms that provably execute the generated script.""" + scalar = legacy.inline_scalar(value) + if scalar is None: + return False + try: + parts = shlex.split(scalar) + except ValueError: + return False + if not parts: + return False + shell = PurePosixPath(parts[0]).name + if shell not in _POSIX_SHELLS: + return False + if len(parts) == 1: + return True + allowed = { + ("bash", "{0}"), + ("bash", "-e", "{0}"), + ("bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "{0}"), + ("sh", "{0}"), + ("sh", "-e", "{0}"), + } + return tuple([shell, *parts[1:]]) in allowed + + +def _dependencies_proven( + job_name: str, dependency_graph: Mapping[str, list[str] | None] +) -> bool: + """Reject missing, self-referential, or cyclic needs topologies.""" + visiting: set[str] = set() + visited: set[str] = set() + + def visit(name: str) -> bool: + if name in visiting: + return False + if name in visited: + return True + dependencies = dependency_graph.get(name) + if dependencies is None: + return False + visiting.add(name) + for dependency in dependencies: + if dependency not in dependency_graph or not visit(dependency): + return False + visiting.remove(name) + visited.add(name) + return True + + return visit(job_name) + + +def github_run_scripts(text: str) -> list[str]: + """Extract only reachable, repo-root scripts under the final shell policy.""" + lines = text.splitlines() + ranges = legacy.scalar_ranges(lines) + scalar_body = { + row + for start, (end, _) in ranges.items() + for row in range(start + 1, end) + } + top = base._properties(lines, 0, len(lines), -1, scalar_body) + jobs = top.get("jobs") + if jobs is None: + return [] + workflow_defaults = base._run_defaults(lines, top, scalar_body) + jobs_children = base._mapping_children(lines, jobs, scalar_body) + if workflow_defaults is None or jobs_children is None: + return [] + + records = {} + dependency_graph: dict[str, list[str] | None] = {} + for job_name, (job_row, job_header) in jobs_children.items(): + job_end = legacy.block_end(lines, job_row, job_header[2]) + properties = base._properties( + lines, job_row + 1, job_end, job_header[2], scalar_body + ) + records[job_name] = (job_row, job_header, job_end, properties) + needs = properties.get("needs") + dependency_graph[job_name] = ( + [] if needs is None else base._parse_needs(needs[1][4]) + ) + + scripts: list[str] = [] + for job_name, (_, _, job_end, properties) in records.items(): + runs_on = properties.get("runs-on") + if runs_on is None or legacy.inline_scalar(runs_on[1][4]) is None: + continue + needs = properties.get("needs") + dependencies = dependency_graph[job_name] + if not _dependencies_proven(job_name, dependency_graph): + continue + job_if = properties.get("if") + if needs is not None: + if not dependencies or job_if is None or base._condition(job_if[1][4]) != "always()": + continue + elif job_if is not None and base._condition(job_if[1][4]) != "true": + continue + + job_defaults = base._run_defaults(lines, properties, scalar_body) + steps = properties.get("steps") + if job_defaults is None or steps is None: + continue + if legacy.inline_scalar(steps[1][4]) is not None or legacy.scalar_indicator(steps[1][4]) is not None: + continue + steps_end = legacy.block_end(lines, steps[0], steps[1][2]) + items = legacy.item_starts(lines, steps[0] + 1, steps_end, scalar_body) + + for index, item_row in enumerate(items): + item_end = items[index + 1] if index + 1 < len(items) else steps_end + first = legacy.yaml_header(lines[item_row]) + if first is None or not first[1]: + continue + step = {first[3]: (item_row, first)} + for row in range(item_row + 1, item_end): + if row in scalar_body: + continue + header = legacy.yaml_header(lines[row]) + if header is not None and not header[1] and header[2] == first[2]: + step[header[3]] = (row, header) + step_if = step.get("if") + if step_if is not None and base._condition(step_if[1][4]) != "true": + continue + run = step.get("run") + if run is None: + continue + shell = base._effective_entry(step, job_defaults, workflow_defaults, "shell") + if shell is not None and not _supported_shell(shell[1][4]): + continue + working_directory = base._effective_entry( + step, job_defaults, workflow_defaults, "working-directory" + ) + if not base._repo_root_working_directory(working_directory): + continue + if run[0] in ranges: + end, style = ranges[run[0]] + scripts.append(legacy.scalar_text(lines, run[0], end, style)) + elif (value := legacy.inline_scalar(run[1][4])) is not None: + scripts.append(value) + return scripts + + +def _terminates(parts: list[str]) -> bool: + if not parts: + return False + if parts[0] in _TERMINATORS: + return True + return parts[0] in _SHELL_WRAPPERS and any( + token in _TERMINATORS for token in parts[1:] + ) + + +def shell_commands(text: str) -> list[str]: + """Reject control flow and direct or wrapper-invoked terminators.""" + stripped = "\n".join(legacy.strip_comment(line) for line in text.splitlines()) + try: + lexer = shlex.shlex(stripped, posix=True, punctuation_chars=";&|(){}") + lexer.whitespace_split = True + lexer.commenters = "" + tokens = list(lexer) + except ValueError: + return [] + if any(token in _CONTROL or token in legacy.CONTROL_WORDS for token in tokens): + return [] + + commands: list[str] = [] + lines = text.splitlines() + index = 0 + while index < len(lines): + first = legacy.strip_comment(lines[index]).strip() + if not first: + index += 1 + continue + parts = [first] + while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): + parts[-1] = parts[-1].rstrip()[:-1] + index += 1 + continuation = legacy.strip_comment(lines[index]).strip() + if continuation: + parts.append(continuation) + command = " ".join(parts) + try: + parsed = shlex.split(command) + except ValueError: + return [] + if parsed and ( + legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) or _terminates(parsed) + ): + return [] + commands.append(command) + index += 1 + return commands + + +def ci_discovery( + discovery: Mapping[str, Any], workflow_texts: str | list[str] +) -> tuple[bool, list[str]]: + """Apply the final extraction policy before matching configured tests.""" + text = "\n".join(workflow_texts) if isinstance(workflow_texts, list) else workflow_texts + scripts = github_run_scripts(text) + commands = [command for script in scripts for command in shell_commands(script)] + strategy = discovery["strategy"] + uses_pytest = strategy == "pytest_default_discovery" or any( + str(pattern).endswith(".py") for pattern in discovery.get("contains_any", []) + ) + if uses_pytest and ( + any(name.startswith("PYTEST_") for name in legacy.yaml_env_names(text)) + or any( + re.search( + r"\bPYTEST_[A-Za-z0-9_]*\b", + "\n".join(legacy.strip_comment(line) for line in script.splitlines()), + ) + for script in scripts + ) + ): + return False, [] + if strategy == "pytest_default_discovery": + matched = any( + base.pytest_command(command, str(discovery["test_path"]), False) + for command in commands + ) + return matched, [str(discovery["command"])] if matched else [] + if strategy == "mix_default_discovery": + matched = any( + base.mix_command(command, str(discovery["test_path"])) + for command in commands + ) + return matched, [str(discovery["command"])] if matched else [] + + matches: list[str] = [] + for value in discovery["contains_any"]: + pattern = str(value) + if pattern.endswith(".py") and any( + base.pytest_command(command, pattern, True) for command in commands + ): + matches.append(pattern) + elif pattern.endswith(".exs") and any( + base.mix_command(command, pattern, True) for command in commands + ): + matches.append(pattern) + elif not pattern.endswith((".py", ".exs")): + try: + required = shlex.split(pattern) + except ValueError: + continue + if any( + (parts := base._tokens(command)) + and parts[: len(required)] == required + for command in commands + ): + matches.append(pattern) + return bool(matches), matches + + +pytest_command = base.pytest_command +mix_command = base.mix_command +__all__ = [ + "ci_discovery", + "github_run_scripts", + "shell_commands", + "pytest_command", + "mix_command", +] diff --git a/standards/lotus-family/conformance/test_system_model.py b/standards/lotus-family/conformance/test_system_model.py index e2c463a5..8b976456 100644 --- a/standards/lotus-family/conformance/test_system_model.py +++ b/standards/lotus-family/conformance/test_system_model.py @@ -101,7 +101,7 @@ def workflow( ), "ci-shell-control-001": workflow( "if false; then\n python -m pytest\nfi" - ), + ), "cml-subset-001": workflow( "python -m pytest tests/test_other.py" ), @@ -120,9 +120,20 @@ def workflow( "python -m pytest", step_extra=" shell: cat {0}\n", ), + "ci-shell-noexec-001": workflow( + "python -m pytest", + step_extra=" shell: bash -n {0}\n", + ), + "ci-shell-version-001": workflow( + "python -m pytest", + step_extra=" shell: bash --version {0}\n", + ), "ci-terminator-exit-001": workflow( "exit 0\npython -m pytest" ), + "ci-wrapped-terminator-001": workflow( + "command exit 0\npython -m pytest" + ), "ci-needs-skip-001": workflow( "python -m pytest", job_extra=" needs: gate\n", @@ -134,6 +145,21 @@ def workflow( " - run: echo skipped\n" ), ), + "ci-needs-cycle-001": workflow( + "python -m pytest", + job_extra=( + " needs: gate\n" + " if: ${{ always() }}\n" + ), + prefix_jobs=( + " gate:\n" + " needs: test\n" + " if: ${{ always() }}\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - run: echo cyclic\n" + ), + ), "ci-needs-always-pass-001": workflow( "python -m pytest", job_extra=( @@ -196,7 +222,7 @@ def workflow( " run:\n" " working-directory: subdir\n" ), - ), + ), "ci-step-workdir-override-pass-001": workflow( "python -m pytest", workflow_extra=( @@ -246,7 +272,7 @@ def materialize( path.write_text( text or workflow(valid_command(discovery)), encoding="utf-8", - ) + ) return repository @@ -267,7 +293,7 @@ def test_graph_has_five_dimensions_centers_and_trajectories( view = validate_graph(self.graph) self.assertEqual(len(view["nodes"]), 49) self.assertEqual(len(view["edges"]), 64) - self.assertEqual(len(self.routes), 32) + self.assertEqual(len(self.routes), 36) self.assertEqual( set(self.graph["dimensions"]), { From d5baf083517c223156865fbf8db3c16b1a580b94 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:20:15 +0300 Subject: [PATCH 31/79] fix: repair Lotus route node identifiers --- standards/lotus-family/causality/system-routes-v0.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standards/lotus-family/causality/system-routes-v0.1.json b/standards/lotus-family/causality/system-routes-v0.1.json index 6ded475b..f96cf2fa 100644 --- a/standards/lotus-family/causality/system-routes-v0.1.json +++ b/standards/lotus-family/causality/system-routes-v0.1.json @@ -1 +1 @@ -{"schema_version":"pythia.lotus_system_routes.compact.v0.1","graph":"lotus-family-system-v0.1.json","route_columns":["id","repo","scenario","path","trajectory","outcome","reason"],"routes":[["CML-PASS-001","cml","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["LS-PASS-001","ls","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-PASS-001","pythia","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-NONRUN-001","cml","ci-nonrun-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","control.non_run_text","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SKIP-001","cml","ci-step-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_skipped","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-SKIP-001","cml","ci-job-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NO-RUNNER-001","cml","ci-no-runner-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-QUOTED-ENV-001","cml","ci-quoted-env-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_override","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-CONTROL-001","cml","ci-shell-control-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_control_flow","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CML-SUBSET-001","cml","cml-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["PYTHIA-SUBSET-001","pythia","pythia-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-FAKE-STEPS-001","cml","ci-fake-steps-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-CUSTOM-SHELL-001","cml","ci-custom-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-TERMINATOR-EXIT-001","cml","ci-terminator-exit-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-SKIP-001","cml","ci-needs-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-ALWAYS-PASS-001","cml","ci-needs-always-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-LINE-SUBSET-001","pythia","pythia-line-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["LS-NODE-SUBSET-001","ls","ls-node-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-WORKFLOW-DEFAULT-SHELL-001","cml","ci-workflow-default-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-DEFAULT-SHELL-001","cml","ci-job-default-shell-001",["input.snapshot_present","input.sdentity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SHELL-OVERRIDE-PASS-001","cml","ci-step-shell-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-WORKFLOW-WORKDIR-001","cml","ci-workflow-workdir-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-WORKDIR-001","cml","ci-job-workdir-001",["input.snapshot_present","input.sdentity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-WORKDIR-OVERRIDE-PASS-001","cml","ci-step-workdir-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["IDENTITY-INVALID-001","cml","invalid_commit",["input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","COMMIT_SHA_INVALID"],["REF-MISSING-001","cml","blank_ref",["input.snapshot_present","input.sdentity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","REPOSITORY_REF_MISSING"],["SNAPSHOT-MISSING-001","cml","missing_snapshot",["input.snapshot_missing","outcome.unknown"],"audit","UNKNOWN","SNAPSHOT_UNAVAILABLE"],["CONTRACT-DRIFT-001","cml","missing_term",["input.snapshot_present","input.sdentity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_missing","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["IDENTITY-LIMIT-001","cml","valid_identity",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","risk.identity_overclaim","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["MANIFEST-INVALID-001","cml","invalid_manifest",["center.invariant_set","control.manifest_invalid","outcome.unknown"],"audit","UNKNOWN","MANIFEST_INVALID"],["MERGE-ELIGIBLE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_fresh","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"merge","MODEL","MODEL_ONLY"],["MERGE-STALE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_stale","state.merge_blocked"],"merge","MODEL","MODEL_ONLY"],["CI-SHELL-NOEXEC-001","cml","ci-shell-noexec-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-VERSION-001","cml","ci-shell-version-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-WRAPPED-TERMINATOR-001","cml","ci-wrapped-terminator-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-CYCLE-001","cml","ci-needs-cycle-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"]]} +{"schema_version":"pythia.lotus_system_routes.compact.v0.1","graph":"lotus-family-system-v0.1.json","route_columns":["id","repo","scenario","path","trajectory","outcome","reason"],"routes":[["CML-PASS-001","cml","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["LS-PASS-001","ls","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-PASS-001","pythia","valid",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-NONRUN-001","cml","ci-nonrun-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","control.non_run_text","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SKIP-001","cml","ci-step-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_skipped","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-SKIP-001","cml","ci-job-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NO-RUNNER-001","cml","ci-no-runner-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-QUOTED-ENV-001","cml","ci-quoted-env-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_override","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-CONTROL-001","cml","ci-shell-control-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_control_flow","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CML-SUBSET-001","cml","cml-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["PYTHIA-SUBSET-001","pythia","pythia-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-FAKE-STEPS-001","cml","ci-fake-steps-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_not_runnable","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-CUSTOM-SHELL-001","cml","ci-custom-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-TERMINATOR-EXIT-001","cml","ci-terminator-exit-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-SKIP-001","cml","ci-needs-skip-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-ALWAYS-PASS-001","cml","ci-needs-always-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["PYTHIA-LINE-SUBSET-001","pythia","pythia-line-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["LS-NODE-SUBSET-001","ls","ls-node-subset-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.partial_test_scope","control.contract_test_excluded","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-WORKFLOW-DEFAULT-SHELL-001","cml","ci-workflow-default-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-DEFAULT-SHELL-001","cml","ci-job-default-shell-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-SHELL-OVERRIDE-PASS-001","cml","ci-step-shell-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["CI-WORKFLOW-WORKDIR-001","cml","ci-workflow-workdir-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-JOB-WORKDIR-001","cml","ci-job-workdir-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_non_root","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-STEP-WORKDIR-OVERRIDE-PASS-001","cml","ci-step-workdir-override-pass-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.no_terminating_builtin","control.full_test_scope","control.contract_test_selected","evidence.same_bytes_hashed","outcome.pass","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["IDENTITY-INVALID-001","cml","invalid_commit",["input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","COMMIT_SHA_INVALID"],["REF-MISSING-001","cml","blank_ref",["input.snapshot_present","input.identity_claims_invalid","outcome.unknown"],"audit","UNKNOWN","REPOSITORY_REF_MISSING"],["SNAPSHOT-MISSING-001","cml","missing_snapshot",["input.snapshot_missing","outcome.unknown"],"audit","UNKNOWN","SNAPSHOT_UNAVAILABLE"],["CONTRACT-DRIFT-001","cml","missing_term",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_missing","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["IDENTITY-LIMIT-001","cml","valid_identity",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","risk.identity_overclaim","authority.advisory_only"],"audit","PASS","LOTUS_CONTRACT_CONFORMANT"],["MANIFEST-INVALID-001","cml","invalid_manifest",["center.invariant_set","control.manifest_invalid","outcome.unknown"],"audit","UNKNOWN","MANIFEST_INVALID"],["MERGE-ELIGIBLE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_fresh","gate.ci_green","gate.security_green","outcome.pass","gate.review_fresh","gate.no_actionable_blockers","state.merge_eligible","authority.advisory_only"],"merge","MODEL","MODEL_ONLY"],["MERGE-STALE-001","cml","valid",["center.exact_head","gate.provenance_verified","time.evidence_stale","state.merge_blocked"],"merge","MODEL","MODEL_ONLY"],["CI-SHELL-NOEXEC-001","cml","ci-shell-noexec-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-SHELL-VERSION-001","cml","ci-shell-version-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_unknown","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-WRAPPED-TERMINATOR-001","cml","ci-wrapped-terminator-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_proven","control.step_enabled","control.direct_run","control.shell_semantics_known","control.working_directory_repo_root","control.test_env_clean","control.shell_straight_line","control.terminating_builtin","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"],["CI-NEEDS-CYCLE-001","cml","ci-needs-cycle-001",["input.snapshot_present","input.identity_claims_well_formed","limitation.identity_unverified","center.invariant_set","control.manifest_valid","control.contract_terms_present","control.workflow_job_runnable","control.dependencies_unproven","risk.false_ci_pass","outcome.drift"],"audit","DRIFT","LOTUS_CONTRACT_DRIFT"]]} \ No newline at end of file From 70a2e5128a7ccddf88b167b257e810d53ef69e96 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:29:50 +0300 Subject: [PATCH 32/79] fix: reject hidden CI command reachability bypasses --- .../lotus_family_workflow_policy.py | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy.py b/standards/lotus-family/conformance/lotus_family_workflow_policy.py index 50b72b4f..36f97f55 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy.py @@ -11,8 +11,9 @@ import lotus_family_workflow_legacy as legacy _POSIX_SHELLS = {"bash", "sh"} -_SHELL_WRAPPERS = {"command", "builtin", "eval"} +_SHELL_WRAPPERS = {"command", "builtin"} _TERMINATORS = {"exit", "exec", "return", "break", "continue"} +_DIRECTORY_MUTATORS = {"cd", "pushd", "popd", "source", "."} _CONTROL = legacy.CONTROL_TOKENS | {"(", ")", "{", "}"} @@ -31,7 +32,8 @@ def _supported_shell(value: str) -> bool: if shell not in _POSIX_SHELLS: return False if len(parts) == 1: - return True + # Only the exact GitHub built-in keywords receive an implicit temp script. + return parts[0] in _POSIX_SHELLS allowed = { ("bash", "{0}"), ("bash", "-e", "{0}"), @@ -158,19 +160,33 @@ def github_run_scripts(text: str) -> list[str]: return scripts -def _terminates(parts: list[str]) -> bool: +def _unsafe_state_change(parts: list[str]) -> bool: + """Reject commands that can end execution or change later command meaning.""" if not parts: return False - if parts[0] in _TERMINATORS: + command = parts[0] + if command == "eval": + # Eval can hide quoted terminators, directory changes, or control flow. return True - return parts[0] in _SHELL_WRAPPERS and any( - token in _TERMINATORS for token in parts[1:] - ) + if command in _TERMINATORS or command in _DIRECTORY_MUTATORS: + return True + if command in _SHELL_WRAPPERS and len(parts) > 1: + wrapped = parts[1] + return ( + wrapped == "eval" + or wrapped in _TERMINATORS + or wrapped in _DIRECTORY_MUTATORS + ) + return False def shell_commands(text: str) -> list[str]: - """Reject control flow and direct or wrapper-invoked terminators.""" + """Reject scripts whose later test commands cannot be proven reachable.""" stripped = "\n".join(legacy.strip_comment(line) for line in text.splitlines()) + # Heredoc bodies are data, not commands. Conservatively reject the whole + # script rather than risk matching a test command inside the payload. + if "<<" in stripped: + return [] try: lexer = shlex.shlex(stripped, posix=True, punctuation_chars=";&|(){}") lexer.whitespace_split = True @@ -202,7 +218,7 @@ def shell_commands(text: str) -> list[str]: except ValueError: return [] if parsed and ( - legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) or _terminates(parsed) + legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) or _unsafe_state_change(parsed) ): return [] commands.append(command) From 517addab575caa5d4ec9c86aab46b9f2f2d10137 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:30:06 +0300 Subject: [PATCH 33/79] test: cover hidden CI reachability bypasses --- .../conformance/test_lotus_family_auditor.py | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/standards/lotus-family/conformance/test_lotus_family_auditor.py b/standards/lotus-family/conformance/test_lotus_family_auditor.py index 3a6c3993..37206d50 100644 --- a/standards/lotus-family/conformance/test_lotus_family_auditor.py +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -4,11 +4,12 @@ import lotus_family_auditor_legacy_tests as old -def workflow(command, env=""): +def workflow(command, env="", shell=""): env_block = "env:\n" + "\n".join(f" {x}" for x in env.splitlines()) + "\n" if env else "" + shell_line = f" shell: {shell}\n" if shell else "" body = "\n".join(f" {x}" if x else "" for x in command.splitlines()) return ("name: CI\n" + env_block + "jobs:\n test:\n runs-on: ubuntu-latest\n" - " steps:\n - name: Run tests\n run: |\n" + body + "\n") + " steps:\n - name: Run tests\n" + shell_line + " run: |\n" + body + "\n") def fixture(discovery): @@ -27,11 +28,13 @@ def fixture(discovery): return workflow(command) -def assert_drift(self, repo_id, command, *, raw=False, env=""): +def assert_drift(self, repo_id, command, *, raw=False, env="", shell=""): with tempfile.TemporaryDirectory() as directory: root = Path(directory) repo = old._materialize_repository(root, self.config(repo_id)) - self.workflow(repo).write_text(command if raw else workflow(command, env), encoding="utf-8") + self.workflow(repo).write_text( + command if raw else workflow(command, env, shell), encoding="utf-8" + ) result = self.audit(repo_id, root) self.assertEqual(result["outcome"], old.DRIFT) self.assertEqual(self.discovery(result)["outcome"], old.DRIFT) @@ -81,6 +84,26 @@ def pytest_plugins(self): self.assert_ci_drift("cml", "PYTEST_PLUGINS=custom_plugin python -m pytest\n") +def quoted_eval_exit(self): + self.assert_ci_drift("cml", "eval 'exit 0'\npython -m pytest\n") + + +def command_eval_exit(self): + self.assert_ci_drift("cml", "command eval 'exit 0'\npython -m pytest\n") + + +def heredoc_body(self): + self.assert_ci_drift("cml", "cat <<'EOF'\npython -m pytest\nEOF\n") + + +def directory_change(self): + self.assert_ci_drift("cml", "cd subdir\npython -m pytest\n") + + +def path_like_builtin_shell(self): + self.assert_ci_drift("cml", "python -m pytest\n", shell="./bash") + + new_tests = { "test_bare_shell_text_is_not_a_run_step": bare, "test_pytest_text_in_workflow_env_value_is_not_executed": workflow_env_text, @@ -90,6 +113,11 @@ def pytest_plugins(self): "test_ls_pytest_addopts_assignment_is_drift": ls_addopts, "test_workflow_pytest_addopts_env_is_drift": yaml_addopts, "test_pytest_plugin_environment_assignment_is_drift": pytest_plugins, + "test_quoted_eval_terminator_is_drift": quoted_eval_exit, + "test_command_eval_terminator_is_drift": command_eval_exit, + "test_pytest_text_inside_heredoc_is_drift": heredoc_body, + "test_directory_change_before_pytest_is_drift": directory_change, + "test_path_like_builtin_shell_without_placeholder_is_drift": path_like_builtin_shell, } for name, method in new_tests.items(): setattr(old.LotusFamilyAuditorTest, name, method) From 5214f307fdd821e68b196f089c73a8539fee071e Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:33:15 +0300 Subject: [PATCH 34/79] fix: preserve workflow boundaries and fail-fast reachability --- .../lotus_family_workflow_policy.py | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy.py b/standards/lotus-family/conformance/lotus_family_workflow_policy.py index 36f97f55..b67d98e5 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy.py @@ -180,8 +180,19 @@ def _unsafe_state_change(parts: list[str]) -> bool: return False +def _safe_prelude(parts: list[str]) -> bool: + """Return true only for shell setup commands that cannot hide a test.""" + return parts in ( + ["set", "-e"], + ["set", "-eu"], + ["set", "-euo", "pipefail"], + ["set", "-o", "pipefail"], + [":"], + ) + + def shell_commands(text: str) -> list[str]: - """Reject scripts whose later test commands cannot be proven reachable.""" + """Return only a first provably reachable substantive command.""" stripped = "\n".join(legacy.strip_comment(line) for line in text.splitlines()) # Heredoc bodies are data, not commands. Conservatively reject the whole # script rather than risk matching a test command inside the payload. @@ -197,7 +208,6 @@ def shell_commands(text: str) -> list[str]: if any(token in _CONTROL or token in legacy.CONTROL_WORDS for token in tokens): return [] - commands: list[str] = [] lines = text.splitlines() index = 0 while index < len(lines): @@ -221,24 +231,27 @@ def shell_commands(text: str) -> list[str]: legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) or _unsafe_state_change(parsed) ): return [] - commands.append(command) - index += 1 - return commands + if parsed and _safe_prelude(parsed): + index += 1 + continue + # Under fail-fast shells, any unknown predecessor may stop execution. + # Therefore only the first substantive command is provably reachable. + return [command] if parsed else [] + return [] -def ci_discovery( - discovery: Mapping[str, Any], workflow_texts: str | list[str] +def _ci_discovery_one( + discovery: Mapping[str, Any], workflow_text: str ) -> tuple[bool, list[str]]: - """Apply the final extraction policy before matching configured tests.""" - text = "\n".join(workflow_texts) if isinstance(workflow_texts, list) else workflow_texts - scripts = github_run_scripts(text) + """Evaluate one workflow document without cross-file state leakage.""" + scripts = github_run_scripts(workflow_text) commands = [command for script in scripts for command in shell_commands(script)] strategy = discovery["strategy"] uses_pytest = strategy == "pytest_default_discovery" or any( str(pattern).endswith(".py") for pattern in discovery.get("contains_any", []) ) if uses_pytest and ( - any(name.startswith("PYTEST_") for name in legacy.yaml_env_names(text)) + any(name.startswith("PYTEST_") for name in legacy.yaml_env_names(workflow_text)) or any( re.search( r"\bPYTEST_[A-Za-z0-9_]*\b", @@ -286,6 +299,22 @@ def ci_discovery( return bool(matches), matches +def ci_discovery( + discovery: Mapping[str, Any], workflow_texts: str | list[str] +) -> tuple[bool, list[str]]: + """Evaluate workflow documents independently and union safe matches.""" + texts = workflow_texts if isinstance(workflow_texts, list) else [workflow_texts] + matched: list[str] = [] + discovered = False + for text in texts: + current, patterns = _ci_discovery_one(discovery, text) + discovered = discovered or current + for pattern in patterns: + if pattern not in matched: + matched.append(pattern) + return discovered, matched + + pytest_command = base.pytest_command mix_command = base.mix_command __all__ = [ From 68187683b73430db169b64ab76d43c8c6df24ed4 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:33:33 +0300 Subject: [PATCH 35/79] fix: reject empty compact model collections --- .../lotus-family/conformance/lotus_family_system_model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/standards/lotus-family/conformance/lotus_family_system_model.py b/standards/lotus-family/conformance/lotus_family_system_model.py index 154b31c9..71d52a0c 100644 --- a/standards/lotus-family/conformance/lotus_family_system_model.py +++ b/standards/lotus-family/conformance/lotus_family_system_model.py @@ -28,8 +28,11 @@ def rows( declared = model.get(f"{key[:-1]}_columns") if key.endswith("s") else None if declared != columns: raise ValueError(f"unexpected {key} columns") + raw_rows = model.get(key) + if not isinstance(raw_rows, list) or not raw_rows: + raise ValueError(f"{key} must be a non-empty list") result = [] - for raw in model.get(key, []): + for raw in raw_rows: if not isinstance(raw, list) or len(raw) != len(columns): raise ValueError(f"invalid {key} row") result.append(dict(zip(columns, raw))) From d99bf64a7b7c883c2232a4e32b175b4d85f75013 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:33:44 +0300 Subject: [PATCH 36/79] test: cover workflow and compact model boundaries --- .../conformance/test_policy_boundaries.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 standards/lotus-family/conformance/test_policy_boundaries.py diff --git a/standards/lotus-family/conformance/test_policy_boundaries.py b/standards/lotus-family/conformance/test_policy_boundaries.py new file mode 100644 index 00000000..c7286caa --- /dev/null +++ b/standards/lotus-family/conformance/test_policy_boundaries.py @@ -0,0 +1,86 @@ +"""Focused fail-closed regression checks for Lotus policy boundaries.""" + +from __future__ import annotations + +import copy +import unittest + +from lotus_family_system_model import EDGE_COLS, NODE_COLS, ROUTE_COLS, rows +from lotus_family_workflow import ci_discovery + + +def workflow(command: str, *, env: str = "") -> str: + env_block = f"env:\n {env}\n" if env else "" + body = "\n".join(f" {line}" for line in command.splitlines()) + return ( + "name: CI\n" + f"{env_block}" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - name: Run tests\n" + " run: |\n" + f"{body}\n" + ) + + +DISCOVERY = { + "strategy": "pytest_default_discovery", + "test_path": "tests/test_lotus_docs_contract.py", + "command": "python -m pytest", +} + + +class WorkflowBoundaryTest(unittest.TestCase): + def test_workflow_documents_are_evaluated_independently(self) -> None: + blocked = workflow( + "echo no tests", + env=( + "PYTEST_ADDOPTS: " + "--ignore=tests/test_lotus_docs_contract.py" + ), + ) + clean = workflow("python -m pytest") + self.assertEqual( + ci_discovery(DISCOVERY, [blocked, clean]), + (True, ["python -m pytest"]), + ) + + def test_unknown_failing_predecessor_blocks_later_pytest(self) -> None: + self.assertEqual( + ci_discovery( + DISCOVERY, + workflow("false\npython -m pytest"), + ), + (False, []), + ) + + def test_known_safe_shell_prelude_keeps_pytest_reachable(self) -> None: + self.assertEqual( + ci_discovery( + DISCOVERY, + workflow("set -euo pipefail\npython -m pytest"), + ), + (True, ["python -m pytest"]), + ) + + +class CompactModelBoundaryTest(unittest.TestCase): + def test_compact_collections_must_be_non_empty_lists(self) -> None: + for key, columns in ( + ("nodes", NODE_COLS), + ("edges", EDGE_COLS), + ("routes", ROUTE_COLS), + ): + with self.subTest(key=key): + model = { + f"{key[:-1]}_columns": copy.deepcopy(columns), + key: [], + } + with self.assertRaisesRegex(ValueError, "non-empty list"): + rows(model, key, columns) + + +if __name__ == "__main__": + unittest.main() From 9b1c9336ada532abd6021f444b8671de3e5355d3 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:34:03 +0300 Subject: [PATCH 37/79] test: assert public audit-only authority mode --- standards/lotus-family/conformance/test_causality_model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/standards/lotus-family/conformance/test_causality_model.py b/standards/lotus-family/conformance/test_causality_model.py index 45283d42..6e32416c 100644 --- a/standards/lotus-family/conformance/test_causality_model.py +++ b/standards/lotus-family/conformance/test_causality_model.py @@ -288,6 +288,10 @@ def test_causal_routes_execute_to_their_expected_outcomes( ], identity_mode, ) + self.assertEqual( + result["authority"]["mode"], + "audit_only", + ) for grant in AUTHORITY_GRANTS: self.assertFalse( result["authority"][grant], From 344559bca839222338bfed5585c145dbc2e2893e Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:45:31 +0300 Subject: [PATCH 38/79] fix: preserve workflow step gating semantics --- .../lotus_family_workflow_policy.py | 269 ++++++++++++------ 1 file changed, 188 insertions(+), 81 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy.py b/standards/lotus-family/conformance/lotus_family_workflow_policy.py index b67d98e5..ae21ec49 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy.py @@ -4,7 +4,6 @@ import re import shlex -from pathlib import PurePosixPath from typing import Any, Mapping import lotus_family_workflow_hardened as base @@ -18,7 +17,7 @@ def _supported_shell(value: str) -> bool: - """Accept only shell forms that provably execute the generated script.""" + """Accept only exact shell forms that provably execute the generated script.""" scalar = legacy.inline_scalar(value) if scalar is None: return False @@ -26,14 +25,10 @@ def _supported_shell(value: str) -> bool: parts = shlex.split(scalar) except ValueError: return False - if not parts: - return False - shell = PurePosixPath(parts[0]).name - if shell not in _POSIX_SHELLS: + if not parts or parts[0] not in _POSIX_SHELLS: return False if len(parts) == 1: - # Only the exact GitHub built-in keywords receive an implicit temp script. - return parts[0] in _POSIX_SHELLS + return True allowed = { ("bash", "{0}"), ("bash", "-e", "{0}"), @@ -41,7 +36,7 @@ def _supported_shell(value: str) -> bool: ("sh", "{0}"), ("sh", "-e", "{0}"), } - return tuple([shell, *parts[1:]]) in allowed + return tuple(parts) in allowed def _dependencies_proven( @@ -70,8 +65,24 @@ def visit(name: str) -> bool: return visit(job_name) -def github_run_scripts(text: str) -> list[str]: - """Extract only reachable, repo-root scripts under the final shell policy.""" +def _boolean_entry( + entry: tuple[int, tuple[int, bool, int, str, str]] | None, + *, + default: bool, +) -> bool | None: + """Resolve a literal GitHub Actions boolean and preserve unknown expressions.""" + if entry is None: + return default + condition = base._condition(entry[1][4]) + if condition == "true": + return True + if condition == "false": + return False + return None + + +def _github_run_step_groups(text: str) -> list[list[tuple[str, bool | None]]]: + """Extract ordered run-step groups from provably runnable jobs.""" lines = text.splitlines() ranges = legacy.scalar_ranges(lines) scalar_body = { @@ -101,7 +112,7 @@ def github_run_scripts(text: str) -> list[str]: [] if needs is None else base._parse_needs(needs[1][4]) ) - scripts: list[str] = [] + groups: list[list[tuple[str, bool | None]]] = [] for job_name, (_, _, job_end, properties) in records.items(): runs_on = properties.get("runs-on") if runs_on is None or legacy.inline_scalar(runs_on[1][4]) is None: @@ -112,7 +123,11 @@ def github_run_scripts(text: str) -> list[str]: continue job_if = properties.get("if") if needs is not None: - if not dependencies or job_if is None or base._condition(job_if[1][4]) != "always()": + if ( + not dependencies + or job_if is None + or base._condition(job_if[1][4]) != "always()" + ): continue elif job_if is not None and base._condition(job_if[1][4]) != "true": continue @@ -121,11 +136,16 @@ def github_run_scripts(text: str) -> list[str]: steps = properties.get("steps") if job_defaults is None or steps is None: continue - if legacy.inline_scalar(steps[1][4]) is not None or legacy.scalar_indicator(steps[1][4]) is not None: + if ( + legacy.inline_scalar(steps[1][4]) is not None + or legacy.scalar_indicator(steps[1][4]) is not None + ): continue steps_end = legacy.block_end(lines, steps[0], steps[1][2]) items = legacy.item_starts(lines, steps[0] + 1, steps_end, scalar_body) + group: list[tuple[str, bool | None]] = [] + blocked = False for index, item_row in enumerate(items): item_end = items[index + 1] if index + 1 < len(items) else steps_end first = legacy.yaml_header(lines[item_row]) @@ -136,28 +156,61 @@ def github_run_scripts(text: str) -> list[str]: if row in scalar_body: continue header = legacy.yaml_header(lines[row]) - if header is not None and not header[1] and header[2] == first[2]: + if ( + header is not None + and not header[1] + and header[2] == first[2] + ): step[header[3]] = (row, header) - step_if = step.get("if") - if step_if is not None and base._condition(step_if[1][4]) != "true": + + step_if = _boolean_entry(step.get("if"), default=True) + if step_if is False: continue + if step_if is not True: + blocked = True + break + run = step.get("run") if run is None: continue - shell = base._effective_entry(step, job_defaults, workflow_defaults, "shell") + shell = base._effective_entry( + step, job_defaults, workflow_defaults, "shell" + ) if shell is not None and not _supported_shell(shell[1][4]): - continue + blocked = True + break working_directory = base._effective_entry( step, job_defaults, workflow_defaults, "working-directory" ) if not base._repo_root_working_directory(working_directory): - continue + blocked = True + break + + continue_on_error = _boolean_entry( + step.get("continue-on-error"), default=False + ) if run[0] in ranges: end, style = ranges[run[0]] - scripts.append(legacy.scalar_text(lines, run[0], end, style)) - elif (value := legacy.inline_scalar(run[1][4])) is not None: - scripts.append(value) - return scripts + script = legacy.scalar_text(lines, run[0], end, style) + else: + script = legacy.inline_scalar(run[1][4]) + if script is None: + blocked = True + break + group.append((script, continue_on_error)) + + if group and not blocked: + groups.append(group) + return groups + + +def github_run_scripts(text: str) -> list[str]: + """Return extracted scripts while preserving the public compatibility API.""" + return [ + script + for group in _github_run_step_groups(text) + for script, _ in group + ] def _unsafe_state_change(parts: list[str]) -> bool: @@ -166,7 +219,6 @@ def _unsafe_state_change(parts: list[str]) -> bool: return False command = parts[0] if command == "eval": - # Eval can hide quoted terminators, directory changes, or control flow. return True if command in _TERMINATORS or command in _DIRECTORY_MUTATORS: return True @@ -191,32 +243,41 @@ def _safe_prelude(parts: list[str]) -> bool: ) -def shell_commands(text: str) -> list[str]: - """Return only a first provably reachable substantive command.""" - stripped = "\n".join(legacy.strip_comment(line) for line in text.splitlines()) - # Heredoc bodies are data, not commands. Conservatively reject the whole - # script rather than risk matching a test command inside the payload. +def _analyze_script(text: str) -> tuple[str, str | None]: + """Classify a script as invalid, prelude-only, or one reachable command.""" + stripped = "\n".join( + legacy.strip_comment(line) for line in text.splitlines() + ) if "<<" in stripped: - return [] + return "invalid", None try: - lexer = shlex.shlex(stripped, posix=True, punctuation_chars=";&|(){}") + lexer = shlex.shlex( + stripped, posix=True, punctuation_chars=";&|(){}" + ) lexer.whitespace_split = True lexer.commenters = "" tokens = list(lexer) except ValueError: - return [] - if any(token in _CONTROL or token in legacy.CONTROL_WORDS for token in tokens): - return [] + return "invalid", None + if any( + token in _CONTROL or token in legacy.CONTROL_WORDS + for token in tokens + ): + return "invalid", None lines = text.splitlines() index = 0 + saw_prelude = False while index < len(lines): first = legacy.strip_comment(lines[index]).strip() if not first: index += 1 continue parts = [first] - while parts[-1].rstrip().endswith("\\") and index + 1 < len(lines): + while ( + parts[-1].rstrip().endswith("\\") + and index + 1 < len(lines) + ): parts[-1] = parts[-1].rstrip()[:-1] index += 1 continuation = legacy.strip_comment(lines[index]).strip() @@ -226,76 +287,118 @@ def shell_commands(text: str) -> list[str]: try: parsed = shlex.split(command) except ValueError: - return [] + return "invalid", None if parsed and ( - legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) or _unsafe_state_change(parsed) + legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) + or _unsafe_state_change(parsed) ): - return [] + return "invalid", None if parsed and _safe_prelude(parsed): + saw_prelude = True index += 1 continue - # Under fail-fast shells, any unknown predecessor may stop execution. - # Therefore only the first substantive command is provably reachable. - return [command] if parsed else [] - return [] + return ("command", command) if parsed else ("invalid", None) + return ("prelude", None) if saw_prelude else ("empty", None) + + +def shell_commands(text: str) -> list[str]: + """Return only a first provably reachable substantive command.""" + kind, command = _analyze_script(text) + return [command] if kind == "command" and command is not None else [] + + +def _command_matches( + discovery: Mapping[str, Any], command: str +) -> list[str]: + """Return configured patterns satisfied by one executable command.""" + strategy = discovery["strategy"] + if strategy == "pytest_default_discovery": + if base.pytest_command( + command, str(discovery["test_path"]), False + ): + return [str(discovery["command"])] + return [] + if strategy == "mix_default_discovery": + if base.mix_command(command, str(discovery["test_path"])): + return [str(discovery["command"])] + return [] + + matches: list[str] = [] + for value in discovery["contains_any"]: + pattern = str(value) + if pattern.endswith(".py") and base.pytest_command( + command, pattern, True + ): + matches.append(pattern) + elif pattern.endswith(".exs") and base.mix_command( + command, pattern, True + ): + matches.append(pattern) + elif not pattern.endswith((".py", ".exs")): + try: + required = shlex.split(pattern) + except ValueError: + continue + parts = base._tokens(command) + if parts and parts[: len(required)] == required: + matches.append(pattern) + return matches def _ci_discovery_one( discovery: Mapping[str, Any], workflow_text: str ) -> tuple[bool, list[str]]: """Evaluate one workflow document without cross-file state leakage.""" - scripts = github_run_scripts(workflow_text) - commands = [command for script in scripts for command in shell_commands(script)] + groups = _github_run_step_groups(workflow_text) + scripts = [ + script for group in groups for script, _ in group + ] strategy = discovery["strategy"] uses_pytest = strategy == "pytest_default_discovery" or any( - str(pattern).endswith(".py") for pattern in discovery.get("contains_any", []) + str(pattern).endswith(".py") + for pattern in discovery.get("contains_any", []) ) if uses_pytest and ( - any(name.startswith("PYTEST_") for name in legacy.yaml_env_names(workflow_text)) + any( + name.startswith("PYTEST_") + for name in legacy.yaml_env_names(workflow_text) + ) or any( re.search( r"\bPYTEST_[A-Za-z0-9_]*\b", - "\n".join(legacy.strip_comment(line) for line in script.splitlines()), + "\n".join( + legacy.strip_comment(line) + for line in script.splitlines() + ), ) for script in scripts ) ): return False, [] - if strategy == "pytest_default_discovery": - matched = any( - base.pytest_command(command, str(discovery["test_path"]), False) - for command in commands - ) - return matched, [str(discovery["command"])] if matched else [] - if strategy == "mix_default_discovery": - matched = any( - base.mix_command(command, str(discovery["test_path"])) - for command in commands - ) - return matched, [str(discovery["command"])] if matched else [] matches: list[str] = [] - for value in discovery["contains_any"]: - pattern = str(value) - if pattern.endswith(".py") and any( - base.pytest_command(command, pattern, True) for command in commands - ): - matches.append(pattern) - elif pattern.endswith(".exs") and any( - base.mix_command(command, pattern, True) for command in commands - ): - matches.append(pattern) - elif not pattern.endswith((".py", ".exs")): - try: - required = shlex.split(pattern) - except ValueError: + for group in groups: + for script, continue_on_error in group: + kind, command = _analyze_script(script) + if kind == "invalid": + break + if kind in {"empty", "prelude"}: continue - if any( - (parts := base._tokens(command)) - and parts[: len(required)] == required - for command in commands - ): - matches.append(pattern) + if command is None: + break + + current = _command_matches(discovery, command) + if current and continue_on_error is False: + for pattern in current: + if pattern not in matches: + matches.append(pattern) + break + + if continue_on_error is True: + continue + # Unknown or disabled failure propagation means a later run step + # is not provably reachable under GitHub's fail-fast semantics. + break return bool(matches), matches @@ -303,7 +406,11 @@ def ci_discovery( discovery: Mapping[str, Any], workflow_texts: str | list[str] ) -> tuple[bool, list[str]]: """Evaluate workflow documents independently and union safe matches.""" - texts = workflow_texts if isinstance(workflow_texts, list) else [workflow_texts] + texts = ( + workflow_texts + if isinstance(workflow_texts, list) + else [workflow_texts] + ) matched: list[str] = [] discovered = False for text in texts: From bb48880fbb7f9b8aca460af409d438f062a39131 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:45:46 +0300 Subject: [PATCH 39/79] test: cover workflow step gate boundaries --- .../conformance/test_policy_boundaries.py | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/standards/lotus-family/conformance/test_policy_boundaries.py b/standards/lotus-family/conformance/test_policy_boundaries.py index c7286caa..536ab3a3 100644 --- a/standards/lotus-family/conformance/test_policy_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_boundaries.py @@ -10,8 +10,11 @@ def workflow(command: str, *, env: str = "") -> str: + """Build one workflow with a single multiline run step.""" env_block = f"env:\n {env}\n" if env else "" - body = "\n".join(f" {line}" for line in command.splitlines()) + body = "\n".join( + f" {line}" for line in command.splitlines() + ) return ( "name: CI\n" f"{env_block}" @@ -25,6 +28,18 @@ def workflow(command: str, *, env: str = "") -> str: ) +def workflow_steps(steps: str) -> str: + """Build one workflow from already indented step-list entries.""" + return ( + "name: CI\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + f"{steps}" + ) + + DISCOVERY = { "strategy": "pytest_default_discovery", "test_path": "tests/test_lotus_docs_contract.py", @@ -33,6 +48,8 @@ def workflow(command: str, *, env: str = "") -> str: class WorkflowBoundaryTest(unittest.TestCase): + """Exercise public fail-closed workflow discovery boundaries.""" + def test_workflow_documents_are_evaluated_independently(self) -> None: blocked = workflow( "echo no tests", @@ -65,8 +82,57 @@ def test_known_safe_shell_prelude_keeps_pytest_reachable(self) -> None: (True, ["python -m pytest"]), ) + def test_earlier_run_step_can_block_later_test_step(self) -> None: + text = workflow_steps( + " - name: Earlier failure\n" + " run: false\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual( + ci_discovery(DISCOVERY, text), + (False, []), + ) + + def test_explicitly_ignored_predecessor_keeps_test_reachable(self) -> None: + text = workflow_steps( + " - name: Allowed failure\n" + " continue-on-error: true\n" + " run: false\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual( + ci_discovery(DISCOVERY, text), + (True, ["python -m pytest"]), + ) + + def test_test_step_cannot_ignore_its_own_failure(self) -> None: + text = workflow_steps( + " - name: Non-gating contract test\n" + " continue-on-error: true\n" + " run: python -m pytest\n" + ) + self.assertEqual( + ci_discovery(DISCOVERY, text), + (False, []), + ) + + def test_path_qualified_shell_template_is_rejected(self) -> None: + text = workflow_steps( + " - name: Repo controlled shell\n" + " shell: ./bash {0}\n" + " run: python -m pytest\n" + ) + self.assertEqual( + ci_discovery(DISCOVERY, text), + (False, []), + ) + class CompactModelBoundaryTest(unittest.TestCase): + """Exercise compact graph and route collection boundaries.""" + def test_compact_collections_must_be_non_empty_lists(self) -> None: for key, columns in ( ("nodes", NODE_COLS), @@ -78,7 +144,9 @@ def test_compact_collections_must_be_non_empty_lists(self) -> None: f"{key[:-1]}_columns": copy.deepcopy(columns), key: [], } - with self.assertRaisesRegex(ValueError, "non-empty list"): + with self.assertRaisesRegex( + ValueError, "non-empty list" + ): rows(model, key, columns) From ac33e227cbd302eab23f366ee534e05923a70f60 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:00:34 +0300 Subject: [PATCH 40/79] fix: reject non-gating workflow execution contexts --- .../lotus_family_workflow_policy_v2.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py new file mode 100644 index 00000000..e40caf5f --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py @@ -0,0 +1,137 @@ +"""Final review hardening overlay for Lotus workflow discovery.""" + +from __future__ import annotations + +import shlex +from typing import Any, Mapping + +import lotus_family_workflow_policy as previous + + +_NON_FAIL_FAST_CUSTOM_SHELLS = { + ("bash", "{0}"), + ("sh", "{0}"), +} + + +def _shell_parts(value: str) -> tuple[str, ...] | None: + """Parse one literal shell value without accepting expressions.""" + scalar = previous.legacy.inline_scalar(value) + if scalar is None: + return None + try: + parts = tuple(shlex.split(scalar)) + except ValueError: + return None + return parts or None + + +def _uses_non_fail_fast_custom_shell(text: str) -> bool: + """Reject custom bash/sh templates that can mask a failed test command.""" + lines = text.splitlines() + ranges = previous.legacy.scalar_ranges(lines) + scalar_body = { + row + for start, (end, _) in ranges.items() + for row in range(start + 1, end) + } + for row, line in enumerate(lines): + if row in scalar_body: + continue + header = previous.legacy.yaml_header(line) + if header is None or header[3] != "shell": + continue + if _shell_parts(header[4]) in _NON_FAIL_FAST_CUSTOM_SHELLS: + return True + return False + + +def _has_non_gating_job(text: str) -> bool: + """Reject jobs whose failure is ignored or controlled by an expression.""" + lines = text.splitlines() + ranges = previous.legacy.scalar_ranges(lines) + scalar_body = { + row + for start, (end, _) in ranges.items() + for row in range(start + 1, end) + } + top = previous.base._properties(lines, 0, len(lines), -1, scalar_body) + jobs = top.get("jobs") + if jobs is None: + return False + children = previous.base._mapping_children(lines, jobs, scalar_body) + if children is None: + return True + for _, (job_row, job_header) in children.items(): + job_end = previous.legacy.block_end(lines, job_row, job_header[2]) + properties = previous.base._properties( + lines, + job_row + 1, + job_end, + job_header[2], + scalar_body, + ) + value = previous._boolean_entry( + properties.get("continue-on-error"), + default=False, + ) + if value is not False: + return True + return False + + +def _workflow_execution_is_gating(text: str) -> bool: + """Require fail-fast shells and jobs whose failure gates the workflow.""" + return not ( + _uses_non_fail_fast_custom_shell(text) + or _has_non_gating_job(text) + ) + + +def github_run_scripts(text: str) -> list[str]: + """Expose scripts only from workflows with a gating execution context.""" + if not _workflow_execution_is_gating(text): + return [] + return previous.github_run_scripts(text) + + +def _ci_discovery_one( + discovery: Mapping[str, Any], workflow_text: str +) -> tuple[bool, list[str]]: + """Evaluate one workflow only after proving its execution remains gating.""" + if not _workflow_execution_is_gating(workflow_text): + return False, [] + return previous._ci_discovery_one(discovery, workflow_text) + + +def ci_discovery( + discovery: Mapping[str, Any], workflow_texts: str | list[str] +) -> tuple[bool, list[str]]: + """Evaluate documents independently and union only gating matches.""" + texts = ( + workflow_texts + if isinstance(workflow_texts, list) + else [workflow_texts] + ) + matched: list[str] = [] + discovered = False + for text in texts: + current, patterns = _ci_discovery_one(discovery, text) + discovered = discovered or current + for pattern in patterns: + if pattern not in matched: + matched.append(pattern) + return discovered, matched + + +shell_commands = previous.shell_commands +pytest_command = previous.pytest_command +mix_command = previous.mix_command + +__all__ = [ + "ci_discovery", + "github_run_scripts", + "shell_commands", + "pytest_command", + "mix_command", +] From cd7d43969ae982a2d032c50d9da8d70c85d2b59d Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:00:54 +0300 Subject: [PATCH 41/79] fix: bind pytest discovery to audited configuration --- .../conformance/lotus_family_runtime_v2.py | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 standards/lotus-family/conformance/lotus_family_runtime_v2.py diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py new file mode 100644 index 00000000..16dbdf55 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -0,0 +1,189 @@ +"""Runtime hardening overlay for Lotus Family conformance v0.1.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any, Mapping + +import lotus_family_runtime as previous +from lotus_family_schema import ( + DRIFT, + PASS, + UNKNOWN, + load_manifest, + manifest_invalid, + read_file, + repository_config, +) + + +_PYTEST_CONFIG_PATHS = ( + "pytest.toml", + "pytest.ini", + "pyproject.toml", + "tox.ini", + "setup.cfg", +) + + +def _has_meaningful_lines(text: str) -> bool: + """Return true when a dedicated pytest config is not empty/comment-only.""" + return any( + stripped and not stripped.startswith(("#", ";")) + for line in text.splitlines() + if (stripped := line.strip()) + ) + + +def _activates_pytest_configuration(path: str, text: str) -> bool: + """Detect config scopes that can alter default pytest collection.""" + if path in {"pytest.toml", "pytest.ini"}: + return _has_meaningful_lines(text) + if path == "pyproject.toml": + return bool( + re.search( + r"(?mi)^\s*\[\s*tool\.pytest\.ini_options\s*\]\s*$", + text, + ) + ) + if path == "tox.ini": + return bool(re.search(r"(?mi)^\s*\[\s*pytest\s*\]\s*$", text)) + if path == "setup.cfg": + return bool( + re.search( + r"(?mi)^\s*\[\s*(?:tool:pytest|pytest)\s*\]\s*$", + text, + ) + ) + return False + + +def _audit_pytest_configuration( + repository_root: Path, + files: list[dict[str, str]], +) -> tuple[list[str], list[str]]: + """Hash known pytest configs and return active or unreadable blockers.""" + observed: list[str] = [] + blockers: list[str] = [] + for relative_path in _PYTEST_CONFIG_PATHS: + candidate = repository_root / relative_path + if not candidate.exists() and not candidate.is_symlink(): + continue + observed.append(relative_path) + text, error = read_file(repository_root, relative_path, files) + if error is not None or text is None: + blockers.append(f"{relative_path}: {error or 'unreadable'}") + continue + if _activates_pytest_configuration(relative_path, text): + blockers.append(relative_path) + return observed, blockers + + +def _deduplicate_files(rows: list[dict[str, str]]) -> list[dict[str, str]]: + """Keep one evidence row per exact path and digest pair.""" + unique = {(row["path"], row["sha256"]): row for row in rows} + return list(unique.values()) + + +def audit_repository( + manifest: Mapping[str, Any], + *, + repository_id: str, + snapshot_root: Path, + repository_ref: str, + commit_sha: str, +) -> dict[str, Any]: + """Audit a snapshot and fail closed on active pytest configuration.""" + audit = previous.audit_repository( + manifest, + repository_id=repository_id, + snapshot_root=snapshot_root, + repository_ref=repository_ref, + commit_sha=commit_sha, + ) + if audit.get("outcome") != PASS: + return audit + + config = repository_config(manifest, repository_id) + if config is None: + return audit + discovery = config.get("ci_discovery", {}) + if discovery.get("strategy") != "pytest_default_discovery": + return audit + + repository_root = ( + snapshot_root.resolve() / str(config["snapshot_dir"]) + ).resolve() + files = list(audit.get("files", [])) + observed, blockers = _audit_pytest_configuration(repository_root, files) + check = { + "check_id": "pytest_configuration", + "outcome": DRIFT if blockers else PASS, + "paths": observed, + "blocked_paths": blockers, + "detail": ( + "active or unreadable pytest configuration can alter default collection" + if blockers + else "known pytest configuration files were absent or hashed without an active pytest scope" + ), + } + audit.setdefault("checks", []).append(check) + audit["files"] = _deduplicate_files(files) + + if blockers: + audit["outcome"] = DRIFT + audit["reason_code"] = "LOTUS_CONTRACT_DRIFT" + audit["detail"] = "non-conforming checks: pytest_configuration" + return audit + + +def main(argv: list[str] | None = None) -> int: + """Run the hardened command-line auditor and write JSON evidence.""" + parser = argparse.ArgumentParser( + description=( + "Audit one repository snapshot against the Lotus Family manifest." + ) + ) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--snapshot-root", type=Path, required=True) + parser.add_argument("--repository-id", required=True) + parser.add_argument("--repository-ref", required=True) + parser.add_argument("--commit-sha", required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args(argv) + + try: + audit = audit_repository( + load_manifest(args.manifest), + repository_id=args.repository_id, + snapshot_root=args.snapshot_root, + repository_ref=args.repository_ref, + commit_sha=args.commit_sha, + ) + except ( + OSError, + UnicodeError, + json.JSONDecodeError, + ValueError, + ) as exc: + audit = manifest_invalid( + str(exc), + args.repository_id, + args.repository_ref, + args.commit_sha, + "", + ) + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(audit, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(audit, ensure_ascii=False, sort_keys=True)) + return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(audit["outcome"], 3) + + +__all__ = ["audit_repository", "main"] From 0fb9556d20f3c13f18a417ce76c23abd773f7604 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:01:24 +0300 Subject: [PATCH 42/79] test: cover final review gating boundaries --- .../test_exact_head_review_hardening.py | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 standards/lotus-family/conformance/test_exact_head_review_hardening.py diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py new file mode 100644 index 00000000..b24adbf3 --- /dev/null +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -0,0 +1,170 @@ +"""Regression checks for the final exact-head review findings.""" + +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from lotus_family_auditor import DRIFT, PASS, audit_repository, load_manifest +from lotus_family_workflow import ci_discovery + + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +MANIFEST_PATH = ROOT / "manifest" / "lotus-family-v0.1.json" +SHA = "a" * 40 +DISCOVERY = { + "strategy": "pytest_default_discovery", + "test_path": "tests/test_lotus_docs_contract.py", + "command": "python -m pytest", +} + + +def _workflow(*, shell: str | None = None, job_continue: str | None = None) -> str: + shell_line = f" shell: {shell}\n" if shell is not None else "" + continue_line = ( + f" continue-on-error: {job_continue}\n" + if job_continue is not None + else "" + ) + return ( + "name: CI\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + f"{continue_line}" + " steps:\n" + " - name: Contract test\n" + f"{shell_line}" + " run: python -m pytest\n" + ) + + +def _write(root: Path, relative_path: str, content: str) -> None: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _materialize_cml(snapshot_root: Path, manifest: dict) -> Path: + config = next( + row for row in manifest["repositories"] if row["id"] == "cml" + ) + repository_root = snapshot_root / config["snapshot_dir"] + terms_by_path: dict[str, list[str]] = {} + for check in config["file_checks"]: + terms_by_path.setdefault(check["path"], []).extend( + check["contains_all"] + ) + for relative_path, terms in terms_by_path.items(): + _write( + repository_root, + relative_path, + "\n".join(dict.fromkeys(terms)) + "\n", + ) + _write( + repository_root, + config["ci_discovery"]["workflow_paths"][0], + _workflow(), + ) + return repository_root + + +class WorkflowReviewHardeningTest(unittest.TestCase): + """Protect workflow failure propagation and gating semantics.""" + + def test_custom_shell_without_fail_fast_is_rejected(self) -> None: + self.assertEqual( + ci_discovery(DISCOVERY, _workflow(shell="bash {0}")), + (False, []), + ) + + def test_custom_shell_with_explicit_fail_fast_is_accepted(self) -> None: + self.assertEqual( + ci_discovery(DISCOVERY, _workflow(shell="bash -e {0}")), + (True, ["python -m pytest"]), + ) + + def test_job_level_continue_on_error_is_rejected(self) -> None: + self.assertEqual( + ci_discovery(DISCOVERY, _workflow(job_continue="true")), + (False, []), + ) + + def test_expression_driven_job_failure_policy_is_rejected(self) -> None: + self.assertEqual( + ci_discovery( + DISCOVERY, + _workflow(job_continue="${{ matrix.experimental }}"), + ), + (False, []), + ) + + +class PytestConfigurationReviewHardeningTest(unittest.TestCase): + """Bind default pytest discovery to hashed repository configuration.""" + + def setUp(self) -> None: + self.manifest = load_manifest(MANIFEST_PATH) + + def _audit(self, snapshot_root: Path) -> dict: + return audit_repository( + self.manifest, + repository_id="cml", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + def test_active_pyproject_pytest_scope_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml(snapshot_root, self.manifest) + _write( + repository_root, + "pyproject.toml", + "[tool.pytest.ini_options]\naddopts = '-k not lotus'\n", + ) + result = self._audit(snapshot_root) + + self.assertEqual(result["outcome"], DRIFT) + self.assertEqual(result["reason_code"], "LOTUS_CONTRACT_DRIFT") + config_check = next( + row + for row in result["checks"] + if row["check_id"] == "pytest_configuration" + ) + self.assertEqual(config_check["outcome"], DRIFT) + self.assertIn("pyproject.toml", config_check["blocked_paths"]) + self.assertIn( + "pyproject.toml", + {row["path"] for row in result["files"]}, + ) + + def test_non_pytest_pyproject_scope_is_hashed_and_keeps_pass(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml(snapshot_root, self.manifest) + _write( + repository_root, + "pyproject.toml", + "[project]\nname = 'cml-fixture'\n", + ) + result = self._audit(snapshot_root) + + self.assertEqual(result["outcome"], PASS) + config_check = next( + row + for row in result["checks"] + if row["check_id"] == "pytest_configuration" + ) + self.assertEqual(config_check["outcome"], PASS) + self.assertIn( + "pyproject.toml", + {row["path"] for row in result["files"]}, + ) + + +if __name__ == "__main__": + unittest.main() From e483db524c624e77236b3c4aa2620f2d52bbefe6 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:01:39 +0300 Subject: [PATCH 43/79] fix: activate final workflow gating policy --- standards/lotus-family/conformance/lotus_family_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow.py b/standards/lotus-family/conformance/lotus_family_workflow.py index 6b874c8b..21a8763e 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow.py +++ b/standards/lotus-family/conformance/lotus_family_workflow.py @@ -1,3 +1,3 @@ """Public fail-closed GitHub Actions discovery API.""" -from lotus_family_workflow_policy import * # noqa: F403 +from lotus_family_workflow_policy_v2 import * # noqa: F403 From 5297e8a7f2c431825e39b2a93dc66b05ead15d81 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:01:56 +0300 Subject: [PATCH 44/79] fix: activate pytest configuration evidence --- standards/lotus-family/conformance/lotus_family_auditor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_auditor.py b/standards/lotus-family/conformance/lotus_family_auditor.py index e039309e..37486ba4 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor.py +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -1,6 +1,6 @@ -from lotus_family_runtime import * # noqa: F403 +from lotus_family_runtime_v2 import * # noqa: F403 from lotus_family_schema import DRIFT, PASS, UNKNOWN, load_manifest -from lotus_family_runtime import main +from lotus_family_runtime_v2 import main if __name__ == "__main__": From 7f68a04f03662c6f0617b3a78f159a86450ebb26 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:15:29 +0300 Subject: [PATCH 45/79] fix(lotus): cover hidden and native pytest config --- .../conformance/lotus_family_runtime_v2.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index 16dbdf55..b04ea331 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -22,13 +22,23 @@ _PYTEST_CONFIG_PATHS = ( "pytest.toml", + ".pytest.toml", "pytest.ini", + ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg", ) +_DEDICATED_PYTEST_CONFIGS = { + "pytest.toml", + ".pytest.toml", + "pytest.ini", + ".pytest.ini", +} + + def _has_meaningful_lines(text: str) -> bool: """Return true when a dedicated pytest config is not empty/comment-only.""" return any( @@ -40,12 +50,12 @@ def _has_meaningful_lines(text: str) -> bool: def _activates_pytest_configuration(path: str, text: str) -> bool: """Detect config scopes that can alter default pytest collection.""" - if path in {"pytest.toml", "pytest.ini"}: + if path in _DEDICATED_PYTEST_CONFIGS: return _has_meaningful_lines(text) if path == "pyproject.toml": return bool( re.search( - r"(?mi)^\s*\[\s*tool\.pytest\.ini_options\s*\]\s*$", + r"(?mi)^\s*\[\s*tool\.pytest(?:\.ini_options)?\s*\]\s*$", text, ) ) From e1a428e6b4534578875d752b5828434906b8a27d Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:15:47 +0300 Subject: [PATCH 46/79] test(lotus): cover hidden and native pytest configs --- .../test_exact_head_review_hardening.py | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index b24adbf3..0ecae846 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -117,6 +117,18 @@ def _audit(self, snapshot_root: Path) -> dict: commit_sha=SHA, ) + def _assert_blocked_config(self, result: dict, path: str) -> None: + self.assertEqual(result["outcome"], DRIFT) + self.assertEqual(result["reason_code"], "LOTUS_CONTRACT_DRIFT") + config_check = next( + row + for row in result["checks"] + if row["check_id"] == "pytest_configuration" + ) + self.assertEqual(config_check["outcome"], DRIFT) + self.assertIn(path, config_check["blocked_paths"]) + self.assertIn(path, {row["path"] for row in result["files"]}) + def test_active_pyproject_pytest_scope_blocks_default_discovery(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) @@ -128,19 +140,33 @@ def test_active_pyproject_pytest_scope_blocks_default_discovery(self) -> None: ) result = self._audit(snapshot_root) - self.assertEqual(result["outcome"], DRIFT) - self.assertEqual(result["reason_code"], "LOTUS_CONTRACT_DRIFT") - config_check = next( - row - for row in result["checks"] - if row["check_id"] == "pytest_configuration" - ) - self.assertEqual(config_check["outcome"], DRIFT) - self.assertIn("pyproject.toml", config_check["blocked_paths"]) - self.assertIn( - "pyproject.toml", - {row["path"] for row in result["files"]}, - ) + self._assert_blocked_config(result, "pyproject.toml") + + def test_native_pyproject_pytest_scope_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml(snapshot_root, self.manifest) + _write( + repository_root, + "pyproject.toml", + "[tool.pytest]\npython_files = ['not_contract.py']\n", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "pyproject.toml") + + def test_hidden_pytest_ini_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml(snapshot_root, self.manifest) + _write( + repository_root, + ".pytest.ini", + "[pytest]\naddopts = --ignore=tests/test_lotus_docs_contract.py\n", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, ".pytest.ini") def test_non_pytest_pyproject_scope_is_hashed_and_keeps_pass(self) -> None: with tempfile.TemporaryDirectory() as directory: From a72890455c96c43a26daf0eda251fb89f2aca882 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:23:01 +0300 Subject: [PATCH 47/79] fix(lotus): parse pyproject pytest config with tomllib --- .../conformance/lotus_family_runtime_v2.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index b04ea331..790823ff 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -5,6 +5,7 @@ import argparse import json import re +import tomllib from pathlib import Path from typing import Any, Mapping @@ -53,12 +54,12 @@ def _activates_pytest_configuration(path: str, text: str) -> bool: if path in _DEDICATED_PYTEST_CONFIGS: return _has_meaningful_lines(text) if path == "pyproject.toml": - return bool( - re.search( - r"(?mi)^\s*\[\s*tool\.pytest(?:\.ini_options)?\s*\]\s*$", - text, - ) - ) + try: + document = tomllib.loads(text) + except tomllib.TOMLDecodeError: + return True + tool = document.get("tool") + return isinstance(tool, dict) and "pytest" in tool if path == "tox.ini": return bool(re.search(r"(?mi)^\s*\[\s*pytest\s*\]\s*$", text)) if path == "setup.cfg": @@ -196,4 +197,4 @@ def main(argv: list[str] | None = None) -> int: return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(audit["outcome"], 3) -__all__ = ["audit_repository", "main"] +__all__ = ["audit_repository", "main"] \ No newline at end of file From 83bc8e61c5bc344776a7bd974fb679dce47b078b Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:23:21 +0300 Subject: [PATCH 48/79] test(lotus): cover TOML semantic pytest tables --- .../test_exact_head_review_hardening.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index 0ecae846..a84fddab 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -155,6 +155,47 @@ def test_native_pyproject_pytest_scope_blocks_default_discovery(self) -> None: self._assert_blocked_config(result, "pyproject.toml") + def test_toml_semantic_pytest_headers_block_default_discovery(self) -> None: + cases = { + "commented header": ( + "[tool.pytest.ini_options] # valid TOML comment\n" + "addopts = '-k not lotus'\n" + ), + "spaced dotted keys": ( + "[tool . pytest]\n" + "python_files = ['not_contract.py']\n" + ), + "quoted key": ( + "[tool.\"pytest\"]\n" + "python_files = ['not_contract.py']\n" + ), + } + for name, content in cases.items(): + with self.subTest(name=name): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml( + snapshot_root, + self.manifest, + ) + _write(repository_root, "pyproject.toml", content) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "pyproject.toml") + + def test_invalid_pyproject_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml(snapshot_root, self.manifest) + _write( + repository_root, + "pyproject.toml", + "[tool.pytest\npython_files = ['not_contract.py']\n", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "pyproject.toml") + def test_hidden_pytest_ini_blocks_default_discovery(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) From 2469d07878140a6b1650942e8374b41f19aa3613 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:49:08 +0300 Subject: [PATCH 49/79] fix: fail closed on pytest conftest hooks --- .../conformance/lotus_family_runtime_v2.py | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index 790823ff..db244771 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -4,6 +4,7 @@ import argparse import json +import os import re import tomllib from pathlib import Path @@ -49,8 +50,15 @@ def _has_meaningful_lines(text: str) -> bool: ) +def _is_conftest(path: str) -> bool: + """Return true for a repository-relative pytest conftest path.""" + return path == "conftest.py" or path.endswith("/conftest.py") + + def _activates_pytest_configuration(path: str, text: str) -> bool: - """Detect config scopes that can alter default pytest collection.""" + """Detect config scopes or hooks that can alter default pytest collection.""" + if _is_conftest(path): + return True if path in _DEDICATED_PYTEST_CONFIGS: return _has_meaningful_lines(text) if path == "pyproject.toml": @@ -72,14 +80,42 @@ def _activates_pytest_configuration(path: str, text: str) -> bool: return False +def _discover_conftest_paths(repository_root: Path) -> list[str]: + """Find conftest files deterministically without traversing symlink dirs.""" + discovered: list[str] = [] + for directory, dirnames, filenames in os.walk( + repository_root, + followlinks=False, + ): + directory_path = Path(directory) + dirnames[:] = sorted( + name + for name in dirnames + if not (directory_path / name).is_symlink() + ) + if "conftest.py" not in filenames: + continue + candidate = directory_path / "conftest.py" + try: + relative_path = candidate.relative_to(repository_root).as_posix() + except ValueError: + continue + discovered.append(relative_path) + return sorted(set(discovered)) + + def _audit_pytest_configuration( repository_root: Path, files: list[dict[str, str]], ) -> tuple[list[str], list[str]]: - """Hash known pytest configs and return active or unreadable blockers.""" + """Hash pytest configs and fail closed on active, unreadable, or hook files.""" observed: list[str] = [] blockers: list[str] = [] - for relative_path in _PYTEST_CONFIG_PATHS: + candidate_paths = ( + *_PYTEST_CONFIG_PATHS, + *_discover_conftest_paths(repository_root), + ) + for relative_path in dict.fromkeys(candidate_paths): candidate = repository_root / relative_path if not candidate.exists() and not candidate.is_symlink(): continue @@ -136,9 +172,9 @@ def audit_repository( "paths": observed, "blocked_paths": blockers, "detail": ( - "active or unreadable pytest configuration can alter default collection" + "active, unreadable, or executable pytest configuration can alter default collection" if blockers - else "known pytest configuration files were absent or hashed without an active pytest scope" + else "known pytest configuration and conftest files were absent or hashed without an active pytest scope" ), } audit.setdefault("checks", []).append(check) From 178e9c6a1a95dd65172cb1f262e72e4dac3d805c Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:49:31 +0300 Subject: [PATCH 50/79] test: cover pytest conftest collection hooks --- .../test_exact_head_review_hardening.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index a84fddab..89b24aba 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -209,6 +209,27 @@ def test_hidden_pytest_ini_blocks_default_discovery(self) -> None: self._assert_blocked_config(result, ".pytest.ini") + def test_conftest_hooks_block_default_discovery(self) -> None: + for relative_path in ("conftest.py", "support/conftest.py"): + with self.subTest(relative_path=relative_path): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml( + snapshot_root, + self.manifest, + ) + _write( + repository_root, + relative_path, + ( + "def pytest_ignore_collect(collection_path, config):\n" + " return True\n" + ), + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, relative_path) + def test_non_pytest_pyproject_scope_is_hashed_and_keeps_pass(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) From 5c0583429c1b4ffae682101563f72f6dd79394d2 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:03:32 +0300 Subject: [PATCH 51/79] fix: audit pytest config for explicit Python tests --- .../conformance/lotus_family_runtime_v2.py | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index db244771..a5e2bc20 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -55,6 +55,32 @@ def _is_conftest(path: str) -> bool: return path == "conftest.py" or path.endswith("/conftest.py") +def _contains_python_test_reference(value: object) -> bool: + """Return true when a discovery value names a Python test target.""" + if not isinstance(value, str): + return False + for token in value.split(): + normalized = token.strip("'\"").split("::", 1)[0] + if normalized.lower().endswith(".py"): + return True + return False + + +def _requires_pytest_configuration_audit( + discovery: Mapping[str, Any], +) -> bool: + """Identify discovery strategies whose execution is affected by pytest config.""" + strategy = discovery.get("strategy") + if strategy == "pytest_default_discovery": + return True + if strategy != "contains_any": + return False + candidates = discovery.get("contains_any") + return isinstance(candidates, list) and any( + _contains_python_test_reference(candidate) for candidate in candidates + ) + + def _activates_pytest_configuration(path: str, text: str) -> bool: """Detect config scopes or hooks that can alter default pytest collection.""" if _is_conftest(path): @@ -158,7 +184,9 @@ def audit_repository( if config is None: return audit discovery = config.get("ci_discovery", {}) - if discovery.get("strategy") != "pytest_default_discovery": + if not isinstance(discovery, Mapping) or not _requires_pytest_configuration_audit( + discovery + ): return audit repository_root = ( @@ -172,7 +200,7 @@ def audit_repository( "paths": observed, "blocked_paths": blockers, "detail": ( - "active, unreadable, or executable pytest configuration can alter default collection" + "active, unreadable, or executable pytest configuration can alter test collection or selection" if blockers else "known pytest configuration and conftest files were absent or hashed without an active pytest scope" ), @@ -233,4 +261,4 @@ def main(argv: list[str] | None = None) -> int: return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(audit["outcome"], 3) -__all__ = ["audit_repository", "main"] \ No newline at end of file +__all__ = ["audit_repository", "main"] From 11203264c82e63e253f0dc2f68dbc82509faa967 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:03:46 +0300 Subject: [PATCH 52/79] test: block pytest config for explicit test targets --- .../test_explicit_pytest_configuration.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 standards/lotus-family/conformance/test_explicit_pytest_configuration.py diff --git a/standards/lotus-family/conformance/test_explicit_pytest_configuration.py b/standards/lotus-family/conformance/test_explicit_pytest_configuration.py new file mode 100644 index 00000000..f92ac111 --- /dev/null +++ b/standards/lotus-family/conformance/test_explicit_pytest_configuration.py @@ -0,0 +1,97 @@ +"""Regression coverage for pytest config affecting explicit test targets.""" + +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from lotus_family_auditor import DRIFT, audit_repository, load_manifest + + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +MANIFEST_PATH = ROOT / "manifest" / "lotus-family-v0.1.json" +SHA = "a" * 40 + + +def _write(root: Path, relative_path: str, content: str) -> None: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _materialize_ls(snapshot_root: Path, manifest: dict) -> Path: + config = next( + row for row in manifest["repositories"] if row["id"] == "ls" + ) + repository_root = snapshot_root / config["snapshot_dir"] + terms_by_path: dict[str, list[str]] = {} + for check in config["file_checks"]: + terms_by_path.setdefault(check["path"], []).extend( + check["contains_all"] + ) + for relative_path, terms in terms_by_path.items(): + _write( + repository_root, + relative_path, + "\n".join(dict.fromkeys(terms)) + "\n", + ) + _write( + repository_root, + config["ci_discovery"]["workflow_paths"][0], + ( + "name: CI\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - name: Lotus contract\n" + " run: python -m pytest tests/test_lotus_docs_contract.py\n" + ), + ) + return repository_root + + +class ExplicitPytestConfigurationTest(unittest.TestCase): + """Fail closed when pytest config narrows an explicit Python test run.""" + + def test_pytest_ini_deselect_blocks_explicit_contract_test(self) -> None: + manifest = load_manifest(MANIFEST_PATH) + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_ls(snapshot_root, manifest) + _write( + repository_root, + "pytest.ini", + ( + "[pytest]\n" + "addopts = --deselect " + "tests/test_lotus_docs_contract.py\n" + ), + ) + result = audit_repository( + manifest, + repository_id="ls", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + self.assertEqual(result["outcome"], DRIFT) + self.assertEqual(result["reason_code"], "LOTUS_CONTRACT_DRIFT") + config_check = next( + row + for row in result["checks"] + if row["check_id"] == "pytest_configuration" + ) + self.assertEqual(config_check["outcome"], DRIFT) + self.assertIn("pytest.ini", config_check["blocked_paths"]) + self.assertIn( + "pytest.ini", + {row["path"] for row in result["files"]}, + ) + + +if __name__ == "__main__": + unittest.main() From 572c8e9cdc715b3581ffa8fc59b06fe8718a5dda Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:10:24 +0300 Subject: [PATCH 53/79] fix: fail closed on anchored pytest env mappings --- .../lotus_family_workflow_policy_v2.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py index e40caf5f..965e131e 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re import shlex from typing import Any, Mapping @@ -12,6 +13,9 @@ ("bash", "{0}"), ("sh", "{0}"), } +_PYTEST_ENV_NAME = re.compile(r"\bPYTEST_[A-Za-z0-9_]*\b") +_YAML_ANCHOR_ONLY = re.compile(r"&\S+\Z") +_YAML_ALIAS_ONLY = re.compile(r"\*\S+\Z") def _shell_parts(value: str) -> tuple[str, ...] | None: @@ -80,6 +84,62 @@ def _has_non_gating_job(text: str) -> bool: return False +def _uses_pytest(discovery: Mapping[str, Any]) -> bool: + """Return true when repository configuration can affect discovery.""" + if discovery.get("strategy") == "pytest_default_discovery": + return True + values = discovery.get("contains_any", []) + return isinstance(values, list) and any( + isinstance(value, str) and value.split("::", 1)[0].endswith(".py") + for value in values + ) + + +def _has_unproven_pytest_env(text: str) -> bool: + """Reject pytest env hidden behind aliases or anchored mapping syntax.""" + lines = text.splitlines() + ranges = previous.legacy.scalar_ranges(lines) + scalar_body = { + row + for start, (end, _) in ranges.items() + for row in range(start + 1, end) + } + for index, line in enumerate(lines): + if index in scalar_body: + continue + header = previous.legacy.yaml_header(line) + if header is None or header[3] != "env": + continue + + inline = previous.legacy.strip_comment(header[4]).strip() + if _YAML_ALIAS_ONLY.fullmatch(inline): + return True + if inline.startswith("&") and not _YAML_ANCHOR_ONLY.fullmatch(inline): + return True + if inline and not _YAML_ANCHOR_ONLY.fullmatch(inline): + if _PYTEST_ENV_NAME.search(inline): + return True + continue + + for row in range(index + 1, len(lines)): + if row in scalar_body: + continue + if not lines[row].strip() or lines[row].lstrip().startswith("#"): + continue + indent = previous.legacy.indent_of(lines[row]) + if indent is None or indent <= header[2]: + break + child = previous.legacy.yaml_header(lines[row]) + if ( + child is not None + and not child[1] + and child[2] > header[2] + and child[3].startswith("PYTEST_") + ): + return True + return False + + def _workflow_execution_is_gating(text: str) -> bool: """Require fail-fast shells and jobs whose failure gates the workflow.""" return not ( @@ -101,6 +161,8 @@ def _ci_discovery_one( """Evaluate one workflow only after proving its execution remains gating.""" if not _workflow_execution_is_gating(workflow_text): return False, [] + if _uses_pytest(discovery) and _has_unproven_pytest_env(workflow_text): + return False, [] return previous._ci_discovery_one(discovery, workflow_text) From 30d7c49916985ca6710e114675b79a291ed35546 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:10:45 +0300 Subject: [PATCH 54/79] test: cover anchored pytest env mappings --- .../conformance/test_policy_boundaries.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/standards/lotus-family/conformance/test_policy_boundaries.py b/standards/lotus-family/conformance/test_policy_boundaries.py index 536ab3a3..3039c0c3 100644 --- a/standards/lotus-family/conformance/test_policy_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_boundaries.py @@ -64,6 +64,36 @@ def test_workflow_documents_are_evaluated_independently(self) -> None: (True, ["python -m pytest"]), ) + def test_anchored_pytest_env_mapping_blocks_discovery(self) -> None: + text = ( + "name: CI\n" + "env: &pytest_env\n" + " PYTEST_ADDOPTS: " + "--ignore=tests/test_lotus_docs_contract.py\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + + def test_aliased_env_mapping_fails_closed(self) -> None: + text = ( + "name: CI\n" + "pytest-env: &pytest_env\n" + " SAFE_VALUE: true\n" + "env: *pytest_env\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + def test_unknown_failing_predecessor_blocks_later_pytest(self) -> None: self.assertEqual( ci_discovery( From 64c764e4d7395e03d297d8589fe6354652f1db75 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:49:43 +0300 Subject: [PATCH 55/79] fix: allow safe setup before Lotus test discovery --- .../lotus_family_workflow_policy_v2.py | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py index 965e131e..1f9f45c8 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py @@ -13,6 +13,7 @@ ("bash", "{0}"), ("sh", "{0}"), } +_PROVEN_FAILURE_COMMANDS = {"false"} _PYTEST_ENV_NAME = re.compile(r"\bPYTEST_[A-Za-z0-9_]*\b") _YAML_ANCHOR_ONLY = re.compile(r"&\S+\Z") _YAML_ALIAS_ONLY = re.compile(r"\*\S+\Z") @@ -148,6 +149,12 @@ def _workflow_execution_is_gating(text: str) -> bool: ) +def _proven_failure(command: str) -> bool: + """Recognize only commands that deterministically stop later job steps.""" + parts = previous.base._tokens(command) + return bool(parts) and parts[0] in _PROVEN_FAILURE_COMMANDS + + def github_run_scripts(text: str) -> list[str]: """Expose scripts only from workflows with a gating execution context.""" if not _workflow_execution_is_gating(text): @@ -158,12 +165,59 @@ def github_run_scripts(text: str) -> list[str]: def _ci_discovery_one( discovery: Mapping[str, Any], workflow_text: str ) -> tuple[bool, list[str]]: - """Evaluate one workflow only after proving its execution remains gating.""" + """Find a gating test after safe setup while rejecting proven blockers.""" if not _workflow_execution_is_gating(workflow_text): return False, [] if _uses_pytest(discovery) and _has_unproven_pytest_env(workflow_text): return False, [] - return previous._ci_discovery_one(discovery, workflow_text) + + groups = previous._github_run_step_groups(workflow_text) + scripts = [script for group in groups for script, _ in group] + if _uses_pytest(discovery) and ( + any( + name.startswith("PYTEST_") + for name in previous.legacy.yaml_env_names(workflow_text) + ) + or any( + _PYTEST_ENV_NAME.search( + "\n".join( + previous.legacy.strip_comment(line) + for line in script.splitlines() + ) + ) + for script in scripts + ) + ): + return False, [] + + matches: list[str] = [] + for group in groups: + for script, continue_on_error in group: + kind, command = previous._analyze_script(script) + if kind == "invalid": + break + if kind in {"empty", "prelude"}: + continue + if command is None: + break + + current = previous._command_matches(discovery, command) + if current: + if continue_on_error is not False: + continue + for pattern in current: + if pattern not in matches: + matches.append(pattern) + break + + if continue_on_error is None: + break + if continue_on_error is False and _proven_failure(command): + break + # A normal setup step may fail the job, but it does not make the + # later configured test non-gating when setup succeeds. + continue + return bool(matches), matches def ci_discovery( From dde8a827a0a3e52348f589c36469e1ee5a5e9efe Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:50:02 +0300 Subject: [PATCH 56/79] test: preserve Pythia discovery after setup steps --- .../conformance/test_policy_boundaries.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/standards/lotus-family/conformance/test_policy_boundaries.py b/standards/lotus-family/conformance/test_policy_boundaries.py index 3039c0c3..d9f790b9 100644 --- a/standards/lotus-family/conformance/test_policy_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_boundaries.py @@ -45,6 +45,11 @@ def workflow_steps(steps: str) -> str: "test_path": "tests/test_lotus_docs_contract.py", "command": "python -m pytest", } +MIX_DISCOVERY = { + "strategy": "mix_default_discovery", + "test_path": "test/lotus_docs_contract_test.exs", + "command": "mix test", +} class WorkflowBoundaryTest(unittest.TestCase): @@ -124,6 +129,18 @@ def test_earlier_run_step_can_block_later_test_step(self) -> None: (False, []), ) + def test_normal_setup_step_keeps_later_mix_test_discoverable(self) -> None: + text = workflow_steps( + " - name: Install local tooling\n" + " run: mix local.hex --force\n" + " - name: Contract test\n" + " run: mix test\n" + ) + self.assertEqual( + ci_discovery(MIX_DISCOVERY, text), + (True, ["mix test"]), + ) + def test_explicitly_ignored_predecessor_keeps_test_reachable(self) -> None: text = workflow_steps( " - name: Allowed failure\n" From 655a1cb67b5410308864338db1c4f6dc71580f90 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:03:42 +0300 Subject: [PATCH 57/79] fix(lotus): fail close unknown test policy and wrapped false --- .../lotus_family_workflow_policy_v3.py | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py new file mode 100644 index 00000000..bafe83fa --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py @@ -0,0 +1,120 @@ +"""Exact-head review fixes for Lotus workflow discovery reachability.""" + +from __future__ import annotations + +from typing import Any, Mapping + +import lotus_family_workflow_hardened as base +import lotus_family_workflow_policy as execution +import lotus_family_workflow_policy_v2 as previous + +_PROVEN_FAILURE_COMMANDS = {"false"} +_SHELL_WRAPPERS = {"command", "builtin"} + + +def _proven_failure(command: str) -> bool: + """Recognize literal or shell-builtin-wrapped deterministic failures.""" + parts = base._tokens(command) + if not parts: + return False + if parts[0] in _PROVEN_FAILURE_COMMANDS: + return True + if parts[0] not in _SHELL_WRAPPERS: + return False + + index = 1 + while index < len(parts) and parts[index].startswith("-"): + index += 1 + return index < len(parts) and parts[index] in _PROVEN_FAILURE_COMMANDS + + +def _ci_discovery_one( + discovery: Mapping[str, Any], workflow_text: str +) -> tuple[bool, list[str]]: + """Find a gating test while failing closed on unknown step policy.""" + if not previous._workflow_execution_is_gating(workflow_text): + return False, [] + if previous._uses_pytest(discovery) and previous._has_unproven_pytest_env( + workflow_text + ): + return False, [] + + groups = execution._github_run_step_groups(workflow_text) + scripts = [script for group in groups for script, _ in group] + if previous._uses_pytest(discovery) and ( + any( + name.startswith("PYTEST_") + for name in execution.legacy.yaml_env_names(workflow_text) + ) + or any( + previous._PYTEST_ENV_NAME.search( + "\n".join( + execution.legacy.strip_comment(line) + for line in script.splitlines() + ) + ) + for script in scripts + ) + ): + return False, [] + + matches: list[str] = [] + for group in groups: + for script, continue_on_error in group: + kind, command = execution._analyze_script(script) + if kind == "invalid": + break + if kind in {"empty", "prelude"}: + continue + if command is None: + break + + current = execution._command_matches(discovery, command) + if current: + if continue_on_error is None: + break + if continue_on_error is True: + continue + for pattern in current: + if pattern not in matches: + matches.append(pattern) + break + + if continue_on_error is None: + break + if continue_on_error is False and _proven_failure(command): + break + # Ordinary setup steps may precede a later gating test. Only a + # proven blocker or unknown execution policy stops reachability. + continue + return bool(matches), matches + + +def ci_discovery( + discovery: Mapping[str, Any], workflow_texts: str | list[str] +) -> tuple[bool, list[str]]: + """Evaluate workflow documents independently and union gating matches.""" + texts = workflow_texts if isinstance(workflow_texts, list) else [workflow_texts] + matched: list[str] = [] + discovered = False + for text in texts: + current, patterns = _ci_discovery_one(discovery, text) + discovered = discovered or current + for pattern in patterns: + if pattern not in matched: + matched.append(pattern) + return discovered, matched + + +github_run_scripts = previous.github_run_scripts +shell_commands = previous.shell_commands +pytest_command = previous.pytest_command +mix_command = previous.mix_command + +__all__ = [ + "ci_discovery", + "github_run_scripts", + "shell_commands", + "pytest_command", + "mix_command", +] From 67e45a6889be6c03341c4fc9b600df39d9cf1667 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:03:51 +0300 Subject: [PATCH 58/79] fix(lotus): route public workflow policy through v3 --- standards/lotus-family/conformance/lotus_family_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow.py b/standards/lotus-family/conformance/lotus_family_workflow.py index 21a8763e..796a283b 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow.py +++ b/standards/lotus-family/conformance/lotus_family_workflow.py @@ -1,3 +1,3 @@ """Public fail-closed GitHub Actions discovery API.""" -from lotus_family_workflow_policy_v2 import * # noqa: F403 +from lotus_family_workflow_policy_v3 import * # noqa: F403 From f3280db01b56f743a6fe36023165861ad00e7dad Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:04:02 +0300 Subject: [PATCH 59/79] test(lotus): cover unknown test policy and wrapped false --- .../conformance/test_policy_v3_boundaries.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 standards/lotus-family/conformance/test_policy_v3_boundaries.py diff --git a/standards/lotus-family/conformance/test_policy_v3_boundaries.py b/standards/lotus-family/conformance/test_policy_v3_boundaries.py new file mode 100644 index 00000000..96de7250 --- /dev/null +++ b/standards/lotus-family/conformance/test_policy_v3_boundaries.py @@ -0,0 +1,54 @@ +"""Regression coverage for the exact-head workflow policy v3 fixes.""" + +from __future__ import annotations + +import unittest + +from lotus_family_workflow import ci_discovery + +DISCOVERY = { + "strategy": "pytest_default_discovery", + "test_path": "tests/test_lotus_docs_contract.py", + "command": "python -m pytest", +} + + +def workflow_steps(steps: str) -> str: + """Build one runnable workflow from indented step-list entries.""" + return ( + "name: CI\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + f"{steps}" + ) + + +class WorkflowPolicyV3BoundaryTest(unittest.TestCase): + """Keep unknown step policy and wrapped blockers fail-closed.""" + + def test_expression_continue_on_error_blocks_later_test(self) -> None: + text = workflow_steps( + " - name: Matrix-controlled contract test\n" + " continue-on-error: ${{ matrix.allow_failure }}\n" + " run: python -m pytest\n" + " - name: Later contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + + def test_wrapped_false_blocks_later_test(self) -> None: + for command in ("command false", "builtin false"): + with self.subTest(command=command): + text = workflow_steps( + " - name: Proven wrapped failure\n" + f" run: {command}\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + + +if __name__ == "__main__": + unittest.main() From 63b72add46759ec160d5f6851ba55acf3a5c7615 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:15:57 +0300 Subject: [PATCH 60/79] fix: audit pytest configs along explicit target ancestors --- .../conformance/lotus_family_runtime_v3.py | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 standards/lotus-family/conformance/lotus_family_runtime_v3.py diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v3.py b/standards/lotus-family/conformance/lotus_family_runtime_v3.py new file mode 100644 index 00000000..c72e84e7 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_runtime_v3.py @@ -0,0 +1,219 @@ +"""Exact-target pytest configuration hardening for Lotus conformance.""" + +from __future__ import annotations + +import argparse +import json +import shlex +from pathlib import Path, PurePosixPath +from typing import Any, Mapping + +import lotus_family_runtime_v2 as previous +from lotus_family_schema import ( + DRIFT, + PASS, + UNKNOWN, + load_manifest, + manifest_invalid, +) + + +def _explicit_python_targets( + discovery: Mapping[str, Any], +) -> list[PurePosixPath]: + """Return safe repository-relative Python targets from contains_any.""" + if discovery.get("strategy") != "contains_any": + return [] + values = discovery.get("contains_any") + if not isinstance(values, list): + return [] + + targets: set[PurePosixPath] = set() + for value in values: + if not isinstance(value, str): + continue + try: + tokens = shlex.split(value) + except ValueError: + continue + for token in tokens: + normalized = token.split("::", 1)[0] + if not normalized.lower().endswith(".py"): + continue + target = PurePosixPath(normalized) + if target.is_absolute() or ".." in target.parts: + continue + targets.add(target) + return sorted(targets, key=lambda path: path.as_posix()) + + +def _target_ancestor_config_paths( + discovery: Mapping[str, Any], +) -> list[str]: + """List pytest config candidates below root and above explicit targets.""" + directories: set[PurePosixPath] = set() + for target in _explicit_python_targets(discovery): + parent = target.parent + while parent != PurePosixPath("."): + directories.add(parent) + parent = parent.parent + + names = tuple( + PurePosixPath(path).name for path in previous._PYTEST_CONFIG_PATHS + ) + candidates = { + (directory / name).as_posix() + for directory in directories + for name in names + } + return sorted(candidates) + + +def _audit_target_ancestor_configuration( + repository_root: Path, + discovery: Mapping[str, Any], + files: list[dict[str, str]], +) -> tuple[list[str], list[str]]: + """Hash and block pytest configs found along explicit-target ancestors.""" + observed: list[str] = [] + blockers: list[str] = [] + for relative_path in _target_ancestor_config_paths(discovery): + candidate = repository_root / relative_path + if not candidate.exists() and not candidate.is_symlink(): + continue + observed.append(relative_path) + text, error = previous.read_file(repository_root, relative_path, files) + if error is not None or text is None: + blockers.append(f"{relative_path}: {error or 'unreadable'}") + continue + basename = PurePosixPath(relative_path).name + if previous._activates_pytest_configuration(basename, text): + blockers.append(relative_path) + return observed, blockers + + +def audit_repository( + manifest: Mapping[str, Any], + *, + repository_id: str, + snapshot_root: Path, + repository_ref: str, + commit_sha: str, +) -> dict[str, Any]: + """Extend v2 with config discovery rooted at explicit Python targets.""" + audit = previous.audit_repository( + manifest, + repository_id=repository_id, + snapshot_root=snapshot_root, + repository_ref=repository_ref, + commit_sha=commit_sha, + ) + if audit.get("outcome") != PASS: + return audit + + config = previous.repository_config(manifest, repository_id) + if config is None: + return audit + discovery = config.get("ci_discovery", {}) + if not isinstance(discovery, Mapping): + return audit + if not _explicit_python_targets(discovery): + return audit + + repository_root = ( + snapshot_root.resolve() / str(config["snapshot_dir"]) + ).resolve() + files = list(audit.get("files", [])) + observed, blockers = _audit_target_ancestor_configuration( + repository_root, + discovery, + files, + ) + if not observed and not blockers: + return audit + + check = next( + ( + row + for row in audit.get("checks", []) + if row.get("check_id") == "pytest_configuration" + ), + None, + ) + if check is None: + check = { + "check_id": "pytest_configuration", + "outcome": PASS, + "paths": [], + "blocked_paths": [], + "detail": "pytest configuration evidence was evaluated", + } + audit.setdefault("checks", []).append(check) + + check["paths"] = list( + dict.fromkeys([*check.get("paths", []), *observed]) + ) + check["blocked_paths"] = list( + dict.fromkeys([*check.get("blocked_paths", []), *blockers]) + ) + audit["files"] = previous._deduplicate_files(files) + + if blockers: + check["outcome"] = DRIFT + check["detail"] = ( + "active, unreadable, or executable pytest configuration can alter " + "test collection or selection" + ) + audit["outcome"] = DRIFT + audit["reason_code"] = "LOTUS_CONTRACT_DRIFT" + audit["detail"] = "non-conforming checks: pytest_configuration" + return audit + + +def main(argv: list[str] | None = None) -> int: + """Run the v3 auditor and write JSON evidence.""" + parser = argparse.ArgumentParser( + description=( + "Audit one repository snapshot against the Lotus Family manifest." + ) + ) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--snapshot-root", type=Path, required=True) + parser.add_argument("--repository-id", required=True) + parser.add_argument("--repository-ref", required=True) + parser.add_argument("--commit-sha", required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args(argv) + + try: + audit = audit_repository( + load_manifest(args.manifest), + repository_id=args.repository_id, + snapshot_root=args.snapshot_root, + repository_ref=args.repository_ref, + commit_sha=args.commit_sha, + ) + except ( + OSError, + UnicodeError, + json.JSONDecodeError, + ValueError, + ) as exc: + audit = manifest_invalid( + str(exc), + args.repository_id, + args.repository_ref, + args.commit_sha, + "", + ) + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(audit, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(audit, ensure_ascii=False, sort_keys=True)) + return {PASS: 0, DRIFT: 2, UNKNOWN: 3}.get(audit["outcome"], 3) + + +__all__ = ["audit_repository", "main"] From 7ec718ea3f268ec7210a47a1ba257524b1c22441 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:16:07 +0300 Subject: [PATCH 61/79] fix: route auditor through explicit-target config hardening --- standards/lotus-family/conformance/lotus_family_auditor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_auditor.py b/standards/lotus-family/conformance/lotus_family_auditor.py index 37486ba4..b7400093 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor.py +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -1,6 +1,6 @@ -from lotus_family_runtime_v2 import * # noqa: F403 +from lotus_family_runtime_v3 import * # noqa: F403 from lotus_family_schema import DRIFT, PASS, UNKNOWN, load_manifest -from lotus_family_runtime_v2 import main +from lotus_family_runtime_v3 import main if __name__ == "__main__": From 3dc334f1cef8e02fcf846e2d542562165b1969a2 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:16:23 +0300 Subject: [PATCH 62/79] test: cover pytest config at explicit target root --- .../test_explicit_pytest_configuration.py | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/standards/lotus-family/conformance/test_explicit_pytest_configuration.py b/standards/lotus-family/conformance/test_explicit_pytest_configuration.py index f92ac111..77b36903 100644 --- a/standards/lotus-family/conformance/test_explicit_pytest_configuration.py +++ b/standards/lotus-family/conformance/test_explicit_pytest_configuration.py @@ -53,6 +53,24 @@ def _materialize_ls(snapshot_root: Path, manifest: dict) -> Path: return repository_root +def _audit_ls(snapshot_root: Path, manifest: dict) -> dict: + return audit_repository( + manifest, + repository_id="ls", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + +def _configuration_check(result: dict) -> dict: + return next( + row + for row in result["checks"] + if row["check_id"] == "pytest_configuration" + ) + + class ExplicitPytestConfigurationTest(unittest.TestCase): """Fail closed when pytest config narrows an explicit Python test run.""" @@ -70,21 +88,11 @@ def test_pytest_ini_deselect_blocks_explicit_contract_test(self) -> None: "tests/test_lotus_docs_contract.py\n" ), ) - result = audit_repository( - manifest, - repository_id="ls", - snapshot_root=snapshot_root, - repository_ref="refs/heads/main", - commit_sha=SHA, - ) + result = _audit_ls(snapshot_root, manifest) self.assertEqual(result["outcome"], DRIFT) self.assertEqual(result["reason_code"], "LOTUS_CONTRACT_DRIFT") - config_check = next( - row - for row in result["checks"] - if row["check_id"] == "pytest_configuration" - ) + config_check = _configuration_check(result) self.assertEqual(config_check["outcome"], DRIFT) self.assertIn("pytest.ini", config_check["blocked_paths"]) self.assertIn( @@ -92,6 +100,33 @@ def test_pytest_ini_deselect_blocks_explicit_contract_test(self) -> None: {row["path"] for row in result["files"]}, ) + def test_target_parent_pytest_ini_blocks_explicit_contract_test(self) -> None: + manifest = load_manifest(MANIFEST_PATH) + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_ls(snapshot_root, manifest) + _write( + repository_root, + "tests/pytest.ini", + ( + "[pytest]\n" + "addopts = --deselect " + "tests/test_lotus_docs_contract.py\n" + ), + ) + result = _audit_ls(snapshot_root, manifest) + + self.assertEqual(result["outcome"], DRIFT) + self.assertEqual(result["reason_code"], "LOTUS_CONTRACT_DRIFT") + config_check = _configuration_check(result) + self.assertEqual(config_check["outcome"], DRIFT) + self.assertIn("tests/pytest.ini", config_check["paths"]) + self.assertIn("tests/pytest.ini", config_check["blocked_paths"]) + self.assertIn( + "tests/pytest.ini", + {row["path"] for row in result["files"]}, + ) + if __name__ == "__main__": unittest.main() From bfdf55406578b62a86ddb17099f2a6949145ef93 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:33:40 +0300 Subject: [PATCH 63/79] fix: block CI command-resolution mutation --- .../lotus_family_workflow_policy_v3.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py index bafe83fa..e8aca405 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from typing import Any, Mapping import lotus_family_workflow_hardened as base @@ -10,6 +11,12 @@ _PROVEN_FAILURE_COMMANDS = {"false"} _SHELL_WRAPPERS = {"command", "builtin"} +_COMMAND_RESOLUTION_MUTATION = re.compile( + r"(?:\bGITHUB_PATH\b|" + r"(?m)^\s*(?:export\s+)?PATH\s*=|" + r"(?m)^\s*alias\s+(?:python(?:3)?|mix)\s*=|" + r"(?m)^\s*(?:function\s+)?(?:python(?:3)?|mix)\s*\(\s*\))" +) def _proven_failure(command: str) -> bool: @@ -28,6 +35,14 @@ def _proven_failure(command: str) -> bool: return index < len(parts) and parts[index] in _PROVEN_FAILURE_COMMANDS +def _mutates_command_resolution(script: str) -> bool: + """Fail closed when setup can replace the later Python or Mix executable.""" + visible = "\n".join( + execution.legacy.strip_comment(line) for line in script.splitlines() + ) + return bool(_COMMAND_RESOLUTION_MUTATION.search(visible)) + + def _ci_discovery_one( discovery: Mapping[str, Any], workflow_text: str ) -> tuple[bool, list[str]]: @@ -61,6 +76,8 @@ def _ci_discovery_one( matches: list[str] = [] for group in groups: for script, continue_on_error in group: + if _mutates_command_resolution(script): + break kind, command = execution._analyze_script(script) if kind == "invalid": break @@ -85,7 +102,8 @@ def _ci_discovery_one( if continue_on_error is False and _proven_failure(command): break # Ordinary setup steps may precede a later gating test. Only a - # proven blocker or unknown execution policy stops reachability. + # proven blocker, command-resolution mutation, or unknown execution + # policy stops reachability. continue return bool(matches), matches From efd7a9bef23b2d95adef5120824cf4ab3fdd2ef0 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:34:14 +0300 Subject: [PATCH 64/79] fix: compile command-resolution guard safely --- .../conformance/lotus_family_workflow_policy_v3.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py index e8aca405..b256fec5 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py @@ -13,9 +13,10 @@ _SHELL_WRAPPERS = {"command", "builtin"} _COMMAND_RESOLUTION_MUTATION = re.compile( r"(?:\bGITHUB_PATH\b|" - r"(?m)^\s*(?:export\s+)?PATH\s*=|" - r"(?m)^\s*alias\s+(?:python(?:3)?|mix)\s*=|" - r"(?m)^\s*(?:function\s+)?(?:python(?:3)?|mix)\s*\(\s*\))" + r"^\s*(?:export\s+)?PATH\s*=|" + r"^\s*alias\s+(?:python(?:3)?|mix)\s*=|" + r"^\s*(?:function\s+)?(?:python(?:3)?|mix)\s*\(\s*\))", + re.MULTILINE, ) From b4b76b39949d3ec18c4e9c6f687d996b503b9e8d Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:34:28 +0300 Subject: [PATCH 65/79] test: guard CI command resolution --- .../conformance/test_policy_v3_boundaries.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/standards/lotus-family/conformance/test_policy_v3_boundaries.py b/standards/lotus-family/conformance/test_policy_v3_boundaries.py index 96de7250..cd098e87 100644 --- a/standards/lotus-family/conformance/test_policy_v3_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_v3_boundaries.py @@ -26,7 +26,7 @@ def workflow_steps(steps: str) -> str: class WorkflowPolicyV3BoundaryTest(unittest.TestCase): - """Keep unknown step policy and wrapped blockers fail-closed.""" + """Keep unknown policy, blockers, and runner resolution fail-closed.""" def test_expression_continue_on_error_blocks_later_test(self) -> None: text = workflow_steps( @@ -49,6 +49,31 @@ def test_wrapped_false_blocks_later_test(self) -> None: ) self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + def test_github_path_mutation_blocks_later_test(self) -> None: + text = workflow_steps( + " - name: Replace command resolution\n" + " run: echo \"$PWD/bin\" >> \"$GITHUB_PATH\"\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + + def test_direct_path_and_runner_alias_mutations_block_later_test(self) -> None: + commands = ( + "export PATH=\"$PWD/bin:$PATH\"", + "alias python='true'", + "python() { true; }", + ) + for command in commands: + with self.subTest(command=command): + text = workflow_steps( + " - name: Mutate runner resolution\n" + f" run: {command}\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + if __name__ == "__main__": unittest.main() From 29832677b569e431733665884e1988ce2677e2b4 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:11:50 +0300 Subject: [PATCH 66/79] fix: fail closed on dynamic CI runner state --- .../lotus_family_workflow_policy_v4.py | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py new file mode 100644 index 00000000..9e68d15d --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py @@ -0,0 +1,140 @@ +"""Exact-head review fixes for dynamic runner-state and test arguments.""" + +from __future__ import annotations + +import re +from typing import Any, Mapping + +import lotus_family_workflow_policy as execution +import lotus_family_workflow_policy_v2 as policy_v2 +import lotus_family_workflow_policy_v3 as previous + +_RUNNER_STATE_MUTATION = re.compile( + r"(?:\bGITHUB_(?:PATH|ENV)\b|" + r"^\s*(?:export\s+)?PATH\s*=|" + r"^\s*alias\s+(?:python(?:3)?|mix)\s*=|" + r"^\s*(?:function\s+)?(?:python(?:3)?|mix)\s*\(\s*\))", + re.MULTILINE, +) +_SHELL_EXPANSION = re.compile( + r"(?\()" +) + + +def _mutates_runner_state(script: str) -> bool: + """Fail closed when setup can alter later command or environment meaning.""" + visible = "\n".join( + execution.legacy.strip_comment(line) for line in script.splitlines() + ) + return bool(_RUNNER_STATE_MUTATION.search(visible)) + + +def _has_unproven_shell_expansion(command: str) -> bool: + """Reject dynamic test arguments whose runtime value is not proven here.""" + visible = execution.legacy.strip_comment(command) + return bool(_SHELL_EXPANSION.search(visible)) + + +def _command_matches( + discovery: Mapping[str, Any], command: str +) -> list[str]: + """Match only static test commands with fully visible selection arguments.""" + if _has_unproven_shell_expansion(command): + return [] + return execution._command_matches(discovery, command) + + +def _ci_discovery_one( + discovery: Mapping[str, Any], workflow_text: str +) -> tuple[bool, list[str]]: + """Find a gating test while failing closed on dynamic runner state.""" + if not policy_v2._workflow_execution_is_gating(workflow_text): + return False, [] + if policy_v2._uses_pytest(discovery) and policy_v2._has_unproven_pytest_env( + workflow_text + ): + return False, [] + + groups = execution._github_run_step_groups(workflow_text) + scripts = [script for group in groups for script, _ in group] + if policy_v2._uses_pytest(discovery) and ( + any( + name.startswith("PYTEST_") + for name in execution.legacy.yaml_env_names(workflow_text) + ) + or any( + policy_v2._PYTEST_ENV_NAME.search( + "\n".join( + execution.legacy.strip_comment(line) + for line in script.splitlines() + ) + ) + for script in scripts + ) + ): + return False, [] + + matches: list[str] = [] + for group in groups: + for script, continue_on_error in group: + if _mutates_runner_state(script): + break + kind, command = execution._analyze_script(script) + if kind == "invalid": + break + if kind in {"empty", "prelude"}: + continue + if command is None: + break + + current = _command_matches(discovery, command) + if current: + if continue_on_error is None: + break + if continue_on_error is True: + continue + for pattern in current: + if pattern not in matches: + matches.append(pattern) + break + + if continue_on_error is None: + break + if continue_on_error is False and previous._proven_failure(command): + break + # Ordinary static setup may precede a later gate. Dynamic runner-state + # changes, proven blockers, and unknown execution policy stop proof. + continue + return bool(matches), matches + + +def ci_discovery( + discovery: Mapping[str, Any], workflow_texts: str | list[str] +) -> tuple[bool, list[str]]: + """Evaluate workflow documents independently and union gating matches.""" + texts = workflow_texts if isinstance(workflow_texts, list) else [workflow_texts] + matched: list[str] = [] + discovered = False + for text in texts: + current, patterns = _ci_discovery_one(discovery, text) + discovered = discovered or current + for pattern in patterns: + if pattern not in matched: + matched.append(pattern) + return discovered, matched + + +github_run_scripts = previous.github_run_scripts +shell_commands = previous.shell_commands +pytest_command = previous.pytest_command +mix_command = previous.mix_command + +__all__ = [ + "ci_discovery", + "github_run_scripts", + "shell_commands", + "pytest_command", + "mix_command", +] From e44d0541f0156bff166e5bb90d5410ce53b75e95 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:11:58 +0300 Subject: [PATCH 67/79] fix: route workflow discovery through policy v4 --- standards/lotus-family/conformance/lotus_family_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow.py b/standards/lotus-family/conformance/lotus_family_workflow.py index 796a283b..83119030 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow.py +++ b/standards/lotus-family/conformance/lotus_family_workflow.py @@ -1,3 +1,3 @@ """Public fail-closed GitHub Actions discovery API.""" -from lotus_family_workflow_policy_v3 import * # noqa: F403 +from lotus_family_workflow_policy_v4 import * # noqa: F403 From 1a22e0edbd06a7bce0b05bf2f63a28048c3cd352 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:12:20 +0300 Subject: [PATCH 68/79] test: cover dynamic CI state and test arguments --- .../conformance/test_policy_boundaries.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/standards/lotus-family/conformance/test_policy_boundaries.py b/standards/lotus-family/conformance/test_policy_boundaries.py index d9f790b9..6964e4f7 100644 --- a/standards/lotus-family/conformance/test_policy_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_boundaries.py @@ -50,6 +50,14 @@ def workflow_steps(steps: str) -> str: "test_path": "test/lotus_docs_contract_test.exs", "command": "mix test", } +EXPLICIT_PY_DISCOVERY = { + "strategy": "contains_any", + "contains_any": ["tests/test_lotus_docs_contract.py"], +} +EXPLICIT_MIX_DISCOVERY = { + "strategy": "contains_any", + "contains_any": ["test/lotus_docs_contract_test.exs"], +} class WorkflowBoundaryTest(unittest.TestCase): @@ -176,6 +184,36 @@ def test_path_qualified_shell_template_is_rejected(self) -> None: (False, []), ) + def test_dynamic_test_arguments_fail_closed(self) -> None: + cases = ( + ( + EXPLICIT_PY_DISCOVERY, + "python -m pytest tests/test_lotus_docs_contract.py $FILTER", + ), + ( + EXPLICIT_MIX_DISCOVERY, + "mix test test/lotus_docs_contract_test.exs ${FILTER}", + ), + ) + for discovery, command in cases: + with self.subTest(command=command): + self.assertEqual( + ci_discovery(discovery, workflow(command)), + (False, []), + ) + + def test_github_env_mutation_blocks_later_test(self) -> None: + text = workflow_steps( + " - name: Mutate future pytest selection\n" + " run: echo 'PYTEST_ADDOPTS=-k smoke' >> \"$GITHUB_ENV\"\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual( + ci_discovery(DISCOVERY, text), + (False, []), + ) + class CompactModelBoundaryTest(unittest.TestCase): """Exercise compact graph and route collection boundaries.""" From ce0eaa75c78ff9a6eb91aaa3394811030164fac5 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 01:25:57 +0700 Subject: [PATCH 69/79] Close Lotus workflow gating bypasses --- .../lotus_family_workflow_policy.py | 27 ++++++++++++++++--- .../conformance/test_policy_boundaries.py | 23 ++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy.py b/standards/lotus-family/conformance/lotus_family_workflow_policy.py index ae21ec49..d5be5a10 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy.py @@ -30,15 +30,24 @@ def _supported_shell(value: str) -> bool: if len(parts) == 1: return True allowed = { - ("bash", "{0}"), ("bash", "-e", "{0}"), ("bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "{0}"), - ("sh", "{0}"), ("sh", "-e", "{0}"), } return tuple(parts) in allowed +def _default_shell_is_posix(runs_on_value: str) -> bool: + """Accept an implicit shell only when the literal runner is POSIX-based.""" + scalar = legacy.inline_scalar(runs_on_value) + if scalar is None or "${{" in scalar: + return False + normalized = scalar.lower() + if "windows" in normalized: + return False + return any(label in normalized for label in ("ubuntu", "macos", "linux")) + + def _dependencies_proven( job_name: str, dependency_graph: Mapping[str, list[str] | None] ) -> bool: @@ -117,6 +126,11 @@ def _github_run_step_groups(text: str) -> list[list[tuple[str, bool | None]]]: runs_on = properties.get("runs-on") if runs_on is None or legacy.inline_scalar(runs_on[1][4]) is None: continue + job_continue_on_error = _boolean_entry( + properties.get("continue-on-error"), default=False + ) + if job_continue_on_error is not False: + continue needs = properties.get("needs") dependencies = dependency_graph[job_name] if not _dependencies_proven(job_name, dependency_graph): @@ -173,10 +187,17 @@ def _github_run_step_groups(text: str) -> list[list[tuple[str, bool | None]]]: run = step.get("run") if run is None: continue + if "uses" in step: + blocked = True + break shell = base._effective_entry( step, job_defaults, workflow_defaults, "shell" ) - if shell is not None and not _supported_shell(shell[1][4]): + if shell is None: + if not _default_shell_is_posix(runs_on[1][4]): + blocked = True + break + elif not _supported_shell(shell[1][4]): blocked = True break working_directory = base._effective_entry( diff --git a/standards/lotus-family/conformance/test_policy_boundaries.py b/standards/lotus-family/conformance/test_policy_boundaries.py index 6964e4f7..54441ed7 100644 --- a/standards/lotus-family/conformance/test_policy_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_boundaries.py @@ -184,6 +184,29 @@ def test_path_qualified_shell_template_is_rejected(self) -> None: (False, []), ) + def test_action_step_cannot_smuggle_a_run_command(self) -> None: + text = workflow_steps( + " - name: Invalid hybrid step\n" + " uses: actions/checkout@v4\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + + def test_implicit_shell_requires_a_posix_runner(self) -> None: + windows = workflow("python -m pytest").replace( + "runs-on: ubuntu-latest", + "runs-on: windows-latest", + ) + explicit_bash = windows.replace( + " run: |\n", + " shell: bash\n run: |\n", + ) + self.assertEqual(ci_discovery(DISCOVERY, windows), (False, [])) + self.assertEqual( + ci_discovery(DISCOVERY, explicit_bash), + (True, ["python -m pytest"]), + ) + def test_dynamic_test_arguments_fail_closed(self) -> None: cases = ( ( From 8bce1ddf0e6d4d526650c00b5ab4348079f7ba5d Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 03:28:43 +0700 Subject: [PATCH 70/79] Harden Lotus test discovery configuration --- .../conformance/lotus_family_runtime_v2.py | 129 +++++++++++++++--- .../lotus_family_workflow_policy_v4.py | 21 ++- .../test_exact_head_review_hardening.py | 125 +++++++++++++++++ .../conformance/test_policy_v3_boundaries.py | 40 ++++++ 4 files changed, 293 insertions(+), 22 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index a5e2bc20..836e4b7c 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -41,6 +41,20 @@ } +_MIX_CONFIG_PATHS = ("mix.exs", "test/test_helper.exs") + + +_MIX_PROJECT_COLLECTION_SETTING = re.compile( + r"\b(?:test_paths|test_pattern)\s*:", +) + + +_EXUNIT_SELECTION_SETTING = re.compile( + r"\bExUnit\.(?:start|configure)\s*\([^)]*\b(?:exclude|include)\s*:", + re.DOTALL, +) + + def _has_meaningful_lines(text: str) -> bool: """Return true when a dedicated pytest config is not empty/comment-only.""" return any( @@ -81,6 +95,23 @@ def _requires_pytest_configuration_audit( ) +def _requires_mix_configuration_audit( + discovery: Mapping[str, Any], +) -> bool: + """Identify discovery strategies affected by Mix test configuration.""" + strategy = discovery.get("strategy") + if strategy == "mix_default_discovery": + return True + if strategy != "contains_any": + return False + candidates = discovery.get("contains_any") + return isinstance(candidates, list) and any( + isinstance(candidate, str) + and candidate.split("::", 1)[0].strip("'\"").lower().endswith(".exs") + for candidate in candidates + ) + + def _activates_pytest_configuration(path: str, text: str) -> bool: """Detect config scopes or hooks that can alter default pytest collection.""" if _is_conftest(path): @@ -155,6 +186,36 @@ def _audit_pytest_configuration( return observed, blockers +def _activates_mix_configuration(path: str, text: str) -> bool: + """Detect Mix and ExUnit settings that can hide contract tests.""" + if path == "mix.exs": + return bool(_MIX_PROJECT_COLLECTION_SETTING.search(text)) + if path == "test/test_helper.exs": + return bool(_EXUNIT_SELECTION_SETTING.search(text)) + return False + + +def _audit_mix_configuration( + repository_root: Path, + files: list[dict[str, str]], +) -> tuple[list[str], list[str]]: + """Hash known Mix configs and block collection-affecting settings.""" + observed: list[str] = [] + blockers: list[str] = [] + for relative_path in _MIX_CONFIG_PATHS: + candidate = repository_root / relative_path + if not candidate.exists() and not candidate.is_symlink(): + continue + observed.append(relative_path) + text, error = read_file(repository_root, relative_path, files) + if error is not None or text is None: + blockers.append(f"{relative_path}: {error or 'unreadable'}") + continue + if _activates_mix_configuration(relative_path, text): + blockers.append(relative_path) + return observed, blockers + + def _deduplicate_files(rows: list[dict[str, str]]) -> list[dict[str, str]]: """Keep one evidence row per exact path and digest pair.""" unique = {(row["path"], row["sha256"]): row for row in rows} @@ -184,34 +245,66 @@ def audit_repository( if config is None: return audit discovery = config.get("ci_discovery", {}) - if not isinstance(discovery, Mapping) or not _requires_pytest_configuration_audit( - discovery - ): + if not isinstance(discovery, Mapping): return audit repository_root = ( snapshot_root.resolve() / str(config["snapshot_dir"]) ).resolve() files = list(audit.get("files", [])) - observed, blockers = _audit_pytest_configuration(repository_root, files) - check = { - "check_id": "pytest_configuration", - "outcome": DRIFT if blockers else PASS, - "paths": observed, - "blocked_paths": blockers, - "detail": ( - "active, unreadable, or executable pytest configuration can alter test collection or selection" - if blockers - else "known pytest configuration and conftest files were absent or hashed without an active pytest scope" - ), - } - audit.setdefault("checks", []).append(check) + all_blockers: list[str] = [] + + if _requires_pytest_configuration_audit(discovery): + observed, blockers = _audit_pytest_configuration( + repository_root, files + ) + check = { + "check_id": "pytest_configuration", + "outcome": DRIFT if blockers else PASS, + "paths": observed, + "blocked_paths": blockers, + "detail": ( + "active, unreadable, or executable pytest configuration can " + "alter test collection or selection" + if blockers + else "known pytest configuration and conftest files were " + "absent or hashed without an active pytest scope" + ), + } + audit.setdefault("checks", []).append(check) + all_blockers.extend(blockers) + + if _requires_mix_configuration_audit(discovery): + observed, blockers = _audit_mix_configuration(repository_root, files) + check = { + "check_id": "mix_configuration", + "outcome": DRIFT if blockers else PASS, + "paths": observed, + "blocked_paths": blockers, + "detail": ( + "active, unreadable, or collection-changing Mix " + "configuration can alter test discovery or selection" + if blockers + else "known Mix project and test-helper configuration was " + "absent or hashed without collection-changing settings" + ), + } + audit.setdefault("checks", []).append(check) + all_blockers.extend(blockers) + audit["files"] = _deduplicate_files(files) - if blockers: + if all_blockers: audit["outcome"] = DRIFT audit["reason_code"] = "LOTUS_CONTRACT_DRIFT" - audit["detail"] = "non-conforming checks: pytest_configuration" + failed_checks = [ + row["check_id"] + for row in audit["checks"] + if row.get("outcome") == DRIFT + ] + audit["detail"] = "non-conforming checks: " + ", ".join( + sorted(set(failed_checks)) + ) return audit diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py index 9e68d15d..15734004 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py @@ -23,6 +23,19 @@ ) +_RUNNER_RESOLUTION_ENV_NAMES = { + "BASH_ENV", + "ENV", + "ERL_LIBS", + "MIX_ARCHIVES", + "MIX_HOME", + "MIX_PATH", + "PATH", + "PYTHONHOME", + "PYTHONPATH", +} + + def _mutates_runner_state(script: str) -> bool: """Fail closed when setup can alter later command or environment meaning.""" visible = "\n".join( @@ -59,11 +72,11 @@ def _ci_discovery_one( groups = execution._github_run_step_groups(workflow_text) scripts = [script for group in groups for script, _ in group] + yaml_env_names = execution.legacy.yaml_env_names(workflow_text) + if any(name in _RUNNER_RESOLUTION_ENV_NAMES for name in yaml_env_names): + return False, [] if policy_v2._uses_pytest(discovery) and ( - any( - name.startswith("PYTEST_") - for name in execution.legacy.yaml_env_names(workflow_text) - ) + any(name.startswith("PYTEST_") for name in yaml_env_names) or any( policy_v2._PYTEST_ENV_NAME.search( "\n".join( diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index 89b24aba..e58cda3f 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -71,6 +71,30 @@ def _materialize_cml(snapshot_root: Path, manifest: dict) -> Path: return repository_root +def _materialize_pythia(snapshot_root: Path, manifest: dict) -> Path: + config = next( + row for row in manifest["repositories"] if row["id"] == "pythia" + ) + repository_root = snapshot_root / config["snapshot_dir"] + terms_by_path: dict[str, list[str]] = {} + for check in config["file_checks"]: + terms_by_path.setdefault(check["path"], []).extend( + check["contains_all"] + ) + for relative_path, terms in terms_by_path.items(): + _write( + repository_root, + relative_path, + "\n".join(dict.fromkeys(terms)) + "\n", + ) + _write( + repository_root, + config["ci_discovery"]["workflow_paths"][0], + _workflow().replace("python -m pytest", "mix test"), + ) + return repository_root + + class WorkflowReviewHardeningTest(unittest.TestCase): """Protect workflow failure propagation and gating semantics.""" @@ -254,5 +278,106 @@ def test_non_pytest_pyproject_scope_is_hashed_and_keeps_pass(self) -> None: ) +class MixConfigurationReviewHardeningTest(unittest.TestCase): + """Bind Mix default discovery to hashed collection configuration.""" + + def setUp(self) -> None: + self.manifest = load_manifest(MANIFEST_PATH) + + def _audit(self, snapshot_root: Path) -> dict: + return audit_repository( + self.manifest, + repository_id="pythia", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + def _assert_blocked_config(self, result: dict, path: str) -> None: + self.assertEqual(result["outcome"], DRIFT) + check = next( + row + for row in result["checks"] + if row["check_id"] == "mix_configuration" + ) + self.assertEqual(check["outcome"], DRIFT) + self.assertIn(path, check["blocked_paths"]) + self.assertIn(path, {row["path"] for row in result["files"]}) + + def test_mix_test_paths_override_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + _write( + repository_root, + "mix.exs", + "def project, do: [test_paths: [\"ignored\"]]\n", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "mix.exs") + + def test_mix_test_pattern_override_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + _write( + repository_root, + "mix.exs", + "def project, do: [test_pattern: \"*_other.exs\"]\n", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "mix.exs") + + def test_exunit_selection_override_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + _write( + repository_root, + "test/test_helper.exs", + "ExUnit.start(exclude: [:lotus_contract])\n", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "test/test_helper.exs") + + def test_plain_mix_configuration_is_hashed_and_keeps_pass(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + _write( + repository_root, + "mix.exs", + "def project, do: [app: :lotus_fixture]\n", + ) + _write( + repository_root, + "test/test_helper.exs", + "ExUnit.start()\n", + ) + result = self._audit(snapshot_root) + + self.assertEqual(result["outcome"], PASS) + check = next( + row + for row in result["checks"] + if row["check_id"] == "mix_configuration" + ) + self.assertEqual(check["outcome"], PASS) + self.assertEqual( + set(check["paths"]), {"mix.exs", "test/test_helper.exs"} + ) + + if __name__ == "__main__": unittest.main() diff --git a/standards/lotus-family/conformance/test_policy_v3_boundaries.py b/standards/lotus-family/conformance/test_policy_v3_boundaries.py index cd098e87..284f0a33 100644 --- a/standards/lotus-family/conformance/test_policy_v3_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_v3_boundaries.py @@ -74,6 +74,46 @@ def test_direct_path_and_runner_alias_mutations_block_later_test(self) -> None: ) self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + def test_yaml_command_resolution_environment_is_rejected(self) -> None: + cases = ( + ( + "workflow PATH", + "env:\n PATH: ./fake-bin:/usr/bin\n", + "", + "", + ), + ( + "job PYTHONPATH", + "", + " env:\n PYTHONPATH: ./fake-modules\n", + "", + ), + ( + "step PATH", + "", + "", + " env:\n PATH: ./fake-bin:/usr/bin\n", + ), + ) + for name, workflow_env, job_env, step_env in cases: + with self.subTest(name=name): + text = ( + "name: CI\n" + f"{workflow_env}" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + f"{job_env}" + " steps:\n" + " - name: Contract test\n" + f"{step_env}" + " run: python -m pytest\n" + ) + self.assertEqual( + ci_discovery(DISCOVERY, text), + (False, []), + ) + if __name__ == "__main__": unittest.main() From 530a808300527d6f10217321b4cbf964fcf002aa Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:10:49 +0700 Subject: [PATCH 71/79] Harden Lotus workflow discovery proof --- .../conformance/lotus_family_runtime_v2.py | 118 +++++++++++++++++- .../lotus_family_workflow_legacy.py | 15 ++- .../lotus_family_workflow_policy.py | 15 ++- .../lotus_family_workflow_policy_v4.py | 5 +- .../test_exact_head_review_hardening.py | 35 ++++++ .../conformance/test_policy_v3_boundaries.py | 21 ++++ 6 files changed, 201 insertions(+), 8 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index 836e4b7c..10415532 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -44,11 +44,18 @@ _MIX_CONFIG_PATHS = ("mix.exs", "test/test_helper.exs") -_MIX_PROJECT_COLLECTION_SETTING = re.compile( - r"\b(?:test_paths|test_pattern)\s*:", +_MIX_PROJECT_DEFINITION = re.compile( + r"(?m)^[ \t]*def[ \t]+project(?:[ \t]*\([ \t]*\))?[ \t]*" + r"(?:(?Pdo)\b|,[ \t]*do:[ \t]*)", ) +_MIX_PROJECT_KEY = re.compile(r"([A-Za-z_][A-Za-z0-9_?!]*)\s*:") + + +_MIX_COLLECTION_KEYS = {"test_paths", "test_pattern"} + + _EXUNIT_SELECTION_SETTING = re.compile( r"\bExUnit\.(?:start|configure)\s*\([^)]*\b(?:exclude|include)\s*:", re.DOTALL, @@ -186,10 +193,115 @@ def _audit_pytest_configuration( return observed, blockers +def _skip_elixir_space_and_comments(text: str, start: int) -> int: + """Advance over whitespace and line comments outside an Elixir value.""" + index = start + while index < len(text): + if text[index].isspace(): + index += 1 + continue + if text[index] == "#": + newline = text.find("\n", index) + index = len(text) if newline < 0 else newline + 1 + continue + break + return index + + +def _mix_keyword_entries( + text: str, start: int +) -> tuple[list[str], int] | None: + """Split one literal top-level Elixir list without evaluating its values.""" + stack = ["["] + matching = {")": "(", "]": "[", "}": "{"} + entries: list[str] = [] + entry_start = start + 1 + quote: str | None = None + escaped = False + comment = False + + for index in range(start + 1, len(text)): + char = text[index] + if comment: + if char == "\n": + comment = False + continue + if quote is not None: + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == quote: + quote = None + continue + if char == "#": + comment = True + continue + if char in {"'", '"'}: + if text[index : index + 3] == char * 3: + return None + quote = char + continue + if char in "([{": + stack.append(char) + continue + if char in ")]}" and matching[char] != stack[-1]: + return None + if char in ")]}" and len(stack) > 1: + stack.pop() + continue + if char == "]" and stack == ["["]: + entries.append(text[entry_start:index]) + return entries, index + 1 + if stack == ["["] and char == ",": + entries.append(text[entry_start:index]) + entry_start = index + 1 + elif stack == ["["] and char == "|": + return None + return None + + +def _literal_mix_project_keys(text: str) -> set[str] | None: + """Prove that project/0 returns one visible literal keyword list.""" + definitions = list(_MIX_PROJECT_DEFINITION.finditer(text)) + if len(definitions) != 1: + return None + definition = definitions[0] + cursor = _skip_elixir_space_and_comments(text, definition.end()) + if cursor >= len(text) or text[cursor] != "[": + return None + parsed = _mix_keyword_entries(text, cursor) + if parsed is None: + return None + entries, list_end = parsed + + if definition.group("block") is not None: + tail = _skip_elixir_space_and_comments(text, list_end) + if not re.match(r"end\b", text[tail:]): + return None + else: + line_end = text.find("\n", list_end) + line_end = len(text) if line_end < 0 else line_end + if text[list_end:line_end].split("#", 1)[0].strip(): + return None + + keys: set[str] = set() + for entry in entries: + entry_start = _skip_elixir_space_and_comments(entry, 0) + if entry_start == len(entry): + continue + match = _MIX_PROJECT_KEY.match(entry, entry_start) + if match is None: + return None + keys.add(match.group(1)) + return keys + + def _activates_mix_configuration(path: str, text: str) -> bool: """Detect Mix and ExUnit settings that can hide contract tests.""" if path == "mix.exs": - return bool(_MIX_PROJECT_COLLECTION_SETTING.search(text)) + project_keys = _literal_mix_project_keys(text) + return project_keys is None or bool(project_keys & _MIX_COLLECTION_KEYS) if path == "test/test_helper.exs": return bool(_EXUNIT_SELECTION_SETTING.search(text)) return False diff --git a/standards/lotus-family/conformance/lotus_family_workflow_legacy.py b/standards/lotus-family/conformance/lotus_family_workflow_legacy.py index 29a79bf8..8dc9af75 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_legacy.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_legacy.py @@ -18,6 +18,10 @@ SAFE_MIX_PREFIXES = ("--seed=", "--max-failures=") CONTROL_TOKENS = {"&&", "||", ";", "|", "&", "(", ")", "{", "}"} CONTROL_WORDS = {"if", "then", "elif", "else", "fi", "for", "while", "until", "do", "done", "case", "esac", "select", "function"} +UNRESOLVED_ENV_MAPPING = "__LOTUS_UNRESOLVED_ENV_MAPPING__" +YAML_ANCHOR_ONLY = re.compile(r"&[A-Za-z_][A-Za-z0-9_-]*\Z") +YAML_ALIAS_ONLY = re.compile(r"\*[A-Za-z_][A-Za-z0-9_-]*\Z") +INLINE_ENV_KEY = re.compile(r"(?:^|[{,]\s*)['\"]?([A-Za-z_][A-Za-z0-9_]*)['\"]?\s*:") def strip_comment(line: str) -> str: @@ -271,8 +275,17 @@ def yaml_env_names(text: str) -> set[str]: if not header or header[3] != "env": continue inline = strip_comment(header[4]).strip() + if YAML_ALIAS_ONLY.fullmatch(inline): + names.add(UNRESOLVED_ENV_MAPPING) + continue + if YAML_ANCHOR_ONLY.fullmatch(inline): + inline = "" if inline: - names.update(re.findall(r"\bPYTEST_[A-Za-z0-9_]*\b", inline)); continue + inline_names = set(INLINE_ENV_KEY.findall(inline)) + if not inline_names and inline != "{}": + names.add(UNRESOLVED_ENV_MAPPING) + names.update(inline_names) + continue for row in range(index + 1, len(lines)): if row in body: continue diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy.py b/standards/lotus-family/conformance/lotus_family_workflow_policy.py index d5be5a10..dfbecfe1 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy.py @@ -184,12 +184,21 @@ def _github_run_step_groups(text: str) -> list[list[tuple[str, bool | None]]]: blocked = True break + uses = step.get("uses") run = step.get("run") + if uses is not None: + action = legacy.inline_scalar(uses[1][4]) + if ( + run is not None + or action is None + or "${{" in action + or action.startswith(("./", "../")) + ): + blocked = True + break + continue if run is None: continue - if "uses" in step: - blocked = True - break shell = base._effective_entry( step, job_defaults, workflow_defaults, "shell" ) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py index 15734004..f49940cb 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py @@ -73,7 +73,10 @@ def _ci_discovery_one( groups = execution._github_run_step_groups(workflow_text) scripts = [script for group in groups for script, _ in group] yaml_env_names = execution.legacy.yaml_env_names(workflow_text) - if any(name in _RUNNER_RESOLUTION_ENV_NAMES for name in yaml_env_names): + if ( + execution.legacy.UNRESOLVED_ENV_MAPPING in yaml_env_names + or any(name in _RUNNER_RESOLUTION_ENV_NAMES for name in yaml_env_names) + ): return False, [] if policy_v2._uses_pytest(discovery) and ( any(name.startswith("PYTEST_") for name in yaml_env_names) diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index e58cda3f..f93e6285 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -334,6 +334,41 @@ def test_mix_test_pattern_override_blocks_default_discovery(self) -> None: self._assert_blocked_config(result, "mix.exs") + def test_mix_keyword_pipeline_override_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + _write( + repository_root, + "mix.exs", + ( + "def project do\n" + " [app: :lotus_fixture, version: \"0.1.0\"]\n" + " |> Keyword.put(:test_paths, [\"ignored\"])\n" + "end\n" + ), + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "mix.exs") + + def test_unproven_mix_project_construction_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + _write( + repository_root, + "mix.exs", + "def project, do: project_options()\n", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "mix.exs") + def test_exunit_selection_override_blocks_default_discovery(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) diff --git a/standards/lotus-family/conformance/test_policy_v3_boundaries.py b/standards/lotus-family/conformance/test_policy_v3_boundaries.py index 284f0a33..cd9349f7 100644 --- a/standards/lotus-family/conformance/test_policy_v3_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_v3_boundaries.py @@ -58,6 +58,15 @@ def test_github_path_mutation_blocks_later_test(self) -> None: ) self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + def test_local_action_blocks_later_test(self) -> None: + text = workflow_steps( + " - name: Repository-controlled setup\n" + " uses: ./fake-runner\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + def test_direct_path_and_runner_alias_mutations_block_later_test(self) -> None: commands = ( "export PATH=\"$PWD/bin:$PATH\"", @@ -94,6 +103,18 @@ def test_yaml_command_resolution_environment_is_rejected(self) -> None: "", " env:\n PATH: ./fake-bin:/usr/bin\n", ), + ( + "anchored workflow PATH", + "env: &runner_env\n PATH: ./fake-bin:/usr/bin\n", + "", + "", + ), + ( + "unresolved job env alias", + "x-runner-env: &runner_env\n FOO: bar\n", + " env: *runner_env\n", + "", + ), ) for name, workflow_env, job_env, step_env in cases: with self.subTest(name=name): From 0862cf6bf9ed3de3564c955459a71af4bb45598b Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:36:41 +0700 Subject: [PATCH 72/79] Close remaining Lotus discovery bypasses --- .../conformance/lotus_family_runtime_v2.py | 20 +++++++++- .../lotus_family_workflow_policy.py | 15 +++----- .../test_exact_head_review_hardening.py | 37 +++++++++++++++++++ .../conformance/test_policy_v3_boundaries.py | 9 +++++ 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index 10415532..f2b5c541 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -33,6 +33,13 @@ ) +_PYTEST_SHADOW_PATHS = ( + "pytest.py", + "pytest/__init__.py", + "pytest/__main__.py", +) + + _DEDICATED_PYTEST_CONFIGS = { "pytest.toml", ".pytest.toml", @@ -121,6 +128,8 @@ def _requires_mix_configuration_audit( def _activates_pytest_configuration(path: str, text: str) -> bool: """Detect config scopes or hooks that can alter default pytest collection.""" + if path in _PYTEST_SHADOW_PATHS: + return True if _is_conftest(path): return True if path in _DEDICATED_PYTEST_CONFIGS: @@ -177,6 +186,7 @@ def _audit_pytest_configuration( blockers: list[str] = [] candidate_paths = ( *_PYTEST_CONFIG_PATHS, + *_PYTEST_SHADOW_PATHS, *_discover_conftest_paths(repository_root), ) for relative_path in dict.fromkeys(candidate_paths): @@ -267,6 +277,8 @@ def _literal_mix_project_keys(text: str) -> set[str] | None: if len(definitions) != 1: return None definition = definitions[0] + if _skip_elixir_space_and_comments(text, 0) != definition.start(): + return None cursor = _skip_elixir_space_and_comments(text, definition.end()) if cursor >= len(text) or text[cursor] != "[": return None @@ -277,13 +289,19 @@ def _literal_mix_project_keys(text: str) -> set[str] | None: if definition.group("block") is not None: tail = _skip_elixir_space_and_comments(text, list_end) - if not re.match(r"end\b", text[tail:]): + end_match = re.match(r"end\b", text[tail:]) + if end_match is None: return None + definition_end = tail + end_match.end() else: line_end = text.find("\n", list_end) line_end = len(text) if line_end < 0 else line_end if text[list_end:line_end].split("#", 1)[0].strip(): return None + definition_end = list_end + + if _skip_elixir_space_and_comments(text, definition_end) != len(text): + return None keys: set[str] = set() for entry in entries: diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy.py b/standards/lotus-family/conformance/lotus_family_workflow_policy.py index dfbecfe1..bbc8e00c 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy.py @@ -187,16 +187,11 @@ def _github_run_step_groups(text: str) -> list[list[tuple[str, bool | None]]]: uses = step.get("uses") run = step.get("run") if uses is not None: - action = legacy.inline_scalar(uses[1][4]) - if ( - run is not None - or action is None - or "${{" in action - or action.startswith(("./", "../")) - ): - blocked = True - break - continue + # Action implementations are outside this local proof. Even a + # static external action can replace later command resolution + # through GITHUB_PATH, so no later test is proven reachable. + blocked = True + break if run is None: continue shell = base._effective_entry( diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index f93e6285..67099562 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -254,6 +254,25 @@ def test_conftest_hooks_block_default_discovery(self) -> None: self._assert_blocked_config(result, relative_path) + def test_repository_pytest_shadows_block_default_discovery(self) -> None: + cases = ( + "pytest.py", + "pytest/__init__.py", + "pytest/__main__.py", + ) + for relative_path in cases: + with self.subTest(relative_path=relative_path): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml( + snapshot_root, + self.manifest, + ) + _write(repository_root, relative_path, "raise SystemExit(0)\n") + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, relative_path) + def test_non_pytest_pyproject_scope_is_hashed_and_keeps_pass(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) @@ -369,6 +388,24 @@ def test_unproven_mix_project_construction_blocks_default_discovery(self) -> Non self._assert_blocked_config(result, "mix.exs") + def test_executable_code_outside_mix_project_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + _write( + repository_root, + "mix.exs", + ( + "System.halt(0)\n" + "def project, do: [app: :lotus_fixture]\n" + ), + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "mix.exs") + def test_exunit_selection_override_blocks_default_discovery(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) diff --git a/standards/lotus-family/conformance/test_policy_v3_boundaries.py b/standards/lotus-family/conformance/test_policy_v3_boundaries.py index cd9349f7..1069e777 100644 --- a/standards/lotus-family/conformance/test_policy_v3_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_v3_boundaries.py @@ -67,6 +67,15 @@ def test_local_action_blocks_later_test(self) -> None: ) self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + def test_external_action_blocks_later_test(self) -> None: + text = workflow_steps( + " - name: Unproven external setup\n" + " uses: attacker/fake-python@v1\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + def test_direct_path_and_runner_alias_mutations_block_later_test(self) -> None: commands = ( "export PATH=\"$PWD/bin:$PATH\"", From 3bb3fedf9b60a3645603af4c2a9deb5f40aefc8e Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 05:16:38 +0700 Subject: [PATCH 73/79] Bound Lotus CI prerequisite trust --- .github/workflows/ci.yml | 8 +- standards/lotus-family/README.md | 10 ++ .../conformance/lotus_family_runtime_v2.py | 102 ++++++++++++++++-- .../conformance/lotus_family_schema.py | 24 +++++ .../lotus_family_workflow_policy.py | 31 ++++-- .../lotus_family_workflow_policy_v4.py | 53 ++++++++- .../test_exact_head_review_hardening.py | 53 +++++++++ .../conformance/test_policy_boundaries.py | 31 ++++++ .../manifest/lotus-family-v0.1.json | 6 ++ 9 files changed, 297 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b872721f..e2c89763 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,18 +10,18 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - uses: erlef/setup-beam@v1 + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1 with: elixir-version: '1.16' otp-version: '26' - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable - name: Cache deps - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | deps diff --git a/standards/lotus-family/README.md b/standards/lotus-family/README.md index c091678c..3915be6c 100644 --- a/standards/lotus-family/README.md +++ b/standards/lotus-family/README.md @@ -30,6 +30,16 @@ Until that verification exists, `PASS` means conformance of the supplied snapshot contents, not cryptographic proof that the snapshot came from the claimed remote commit. +Workflow action prerequisites are accepted only when the repository adapter +lists the exact `owner/repository@<40-hex-SHA>` identity. This is an explicit, +immutable trust input for structural CI reachability, not local execution or a +claim that the action cannot mutate runner state. Unlisted, local, expression- +selected, tag-selected, or branch-selected actions fail closed. Earlier shell +steps are limited to closed literal prerequisite forms. That allowlist is also +a structural reachability assumption, not proof of side-effect freedom; +arbitrary commands or direct writes to audited inputs never establish a later +test gate. + ## Causal spacetime testing model The test model has two compatible layers: diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index f2b5c541..1662910f 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -7,7 +7,7 @@ import os import re import tomllib -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import Any, Mapping import lotus_family_runtime as previous @@ -60,6 +60,19 @@ _MIX_PROJECT_KEY = re.compile(r"([A-Za-z_][A-Za-z0-9_?!]*)\s*:") +_MIX_REQUIRE_FILE = re.compile( + r'Code\.require_file\("(?P[^"\\]+)",[ \t]*__DIR__\)' +) + + +_MIX_MODULE = re.compile( + r"defmodule[ \t]+[A-Z][A-Za-z0-9_.]*[ \t]+do\b" +) + + +_MIX_USE_PROJECT = re.compile(r"use[ \t]+Mix\.Project\b") + + _MIX_COLLECTION_KEYS = {"test_paths", "test_pattern"} @@ -271,14 +284,61 @@ def _mix_keyword_entries( return None -def _literal_mix_project_keys(text: str) -> set[str] | None: - """Prove that project/0 returns one visible literal keyword list.""" +def _mix_wrapper( + text: str, + definition_start: int, + definition_end: int, +) -> list[str] | None: + """Accept a bare project/0 or one ordinary Mix.Project module wrapper.""" + cursor = _skip_elixir_space_and_comments(text, 0) + required_files: list[str] = [] + while match := _MIX_REQUIRE_FILE.match(text, cursor): + path = PurePosixPath(match.group("path")) + if ( + path.is_absolute() + or path == PurePosixPath(".") + or ".." in path.parts + or path.suffix not in {".ex", ".exs"} + ): + return None + required_files.append(path.as_posix()) + cursor = _skip_elixir_space_and_comments(text, match.end()) + + module = _MIX_MODULE.match(text, cursor) + if module is None: + if required_files: + return None + return ( + [] + if _skip_elixir_space_and_comments(text, 0) == definition_start + and _skip_elixir_space_and_comments(text, definition_end) == len(text) + else None + ) + + cursor = _skip_elixir_space_and_comments(text, module.end()) + use_project = _MIX_USE_PROJECT.match(text, cursor) + if use_project is None: + return None + cursor = _skip_elixir_space_and_comments(text, use_project.end()) + if cursor != _skip_elixir_space_and_comments(text, definition_start): + return None + + suffix = text[definition_end:] + if not re.search(r"(?m)^\s*end\s*(?:#.*)?\s*\Z", suffix): + return None + if len(required_files) != len(set(required_files)): + return None + return required_files + + +def _literal_mix_project( + text: str, +) -> tuple[set[str], list[str]] | None: + """Prove one literal project/0 inside a bounded ordinary Mix wrapper.""" definitions = list(_MIX_PROJECT_DEFINITION.finditer(text)) if len(definitions) != 1: return None definition = definitions[0] - if _skip_elixir_space_and_comments(text, 0) != definition.start(): - return None cursor = _skip_elixir_space_and_comments(text, definition.end()) if cursor >= len(text) or text[cursor] != "[": return None @@ -300,7 +360,12 @@ def _literal_mix_project_keys(text: str) -> set[str] | None: return None definition_end = list_end - if _skip_elixir_space_and_comments(text, definition_end) != len(text): + required_files = _mix_wrapper( + text, + definition.start(), + definition_end, + ) + if required_files is None: return None keys: set[str] = set() @@ -312,7 +377,13 @@ def _literal_mix_project_keys(text: str) -> set[str] | None: if match is None: return None keys.add(match.group(1)) - return keys + return keys, required_files + + +def _literal_mix_project_keys(text: str) -> set[str] | None: + """Return keys from one proven literal Mix project/0 definition.""" + project = _literal_mix_project(text) + return None if project is None else project[0] def _activates_mix_configuration(path: str, text: str) -> bool: @@ -332,6 +403,7 @@ def _audit_mix_configuration( """Hash known Mix configs and block collection-affecting settings.""" observed: list[str] = [] blockers: list[str] = [] + required_files: list[str] = [] for relative_path in _MIX_CONFIG_PATHS: candidate = repository_root / relative_path if not candidate.exists() and not candidate.is_symlink(): @@ -341,7 +413,21 @@ def _audit_mix_configuration( if error is not None or text is None: blockers.append(f"{relative_path}: {error or 'unreadable'}") continue - if _activates_mix_configuration(relative_path, text): + if relative_path == "mix.exs": + project = _literal_mix_project(text) + if project is None or project[0] & _MIX_COLLECTION_KEYS: + blockers.append(relative_path) + else: + required_files.extend(project[1]) + elif _activates_mix_configuration(relative_path, text): + blockers.append(relative_path) + for relative_path in required_files: + observed.append(relative_path) + text, error = read_file(repository_root, relative_path, files) + if error is not None or text is None: + blockers.append(f"{relative_path}: {error or 'unreadable'}") + continue + if re.search(r"\b(?:test_paths|test_pattern)\b", text): blockers.append(relative_path) return observed, blockers diff --git a/standards/lotus-family/conformance/lotus_family_schema.py b/standards/lotus-family/conformance/lotus_family_schema.py index ac04b070..74946fa3 100644 --- a/standards/lotus-family/conformance/lotus_family_schema.py +++ b/standards/lotus-family/conformance/lotus_family_schema.py @@ -8,6 +8,9 @@ PASS, DRIFT, UNKNOWN = "PASS", "DRIFT", "UNKNOWN" COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") +ACTION_SHA_REF = re.compile( + r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[0-9a-f]{40}$" +) def load_json_object(path: Path) -> dict[str, Any]: @@ -83,6 +86,27 @@ def validate_manifest(manifest: Mapping[str, Any]) -> None: raise ValueError(f"{prefix}.ci_discovery.workflow_paths must be a non-empty list") for wi, workflow in enumerate(workflows): relative_path(workflow, f"{prefix}.ci_discovery.workflow_paths[{wi}]") + trusted_actions = discovery.get("trusted_prerequisite_actions", []) + if not isinstance(trusted_actions, list): + raise ValueError( + f"{prefix}.ci_discovery.trusted_prerequisite_actions must be a list" + ) + if any(not isinstance(action, str) for action in trusted_actions): + raise ValueError( + f"{prefix}.ci_discovery.trusted_prerequisite_actions must contain strings" + ) + if len(trusted_actions) != len(set(trusted_actions)): + raise ValueError( + f"{prefix}.ci_discovery.trusted_prerequisite_actions must be unique" + ) + for ai, action in enumerate(trusted_actions): + field = ( + f"{prefix}.ci_discovery.trusted_prerequisite_actions[{ai}]" + ) + if not ACTION_SHA_REF.fullmatch(action): + raise ValueError( + f"{field} must be an owner/repository action pinned to a full SHA" + ) strategy = discovery.get("strategy") if strategy == "contains_any": patterns = discovery.get("contains_any") diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy.py b/standards/lotus-family/conformance/lotus_family_workflow_policy.py index bbc8e00c..9abc432c 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy.py @@ -90,8 +90,17 @@ def _boolean_entry( return None -def _github_run_step_groups(text: str) -> list[list[tuple[str, bool | None]]]: +def _github_run_step_groups( + text: str, + trusted_prerequisite_actions: tuple[str, ...] | list[str] = (), +) -> list[list[tuple[str, bool | None]]]: """Extract ordered run-step groups from provably runnable jobs.""" + if any( + not isinstance(action, str) + for action in trusted_prerequisite_actions + ): + return [] + trusted_actions = set(trusted_prerequisite_actions) lines = text.splitlines() ranges = legacy.scalar_ranges(lines) scalar_body = { @@ -187,11 +196,21 @@ def _github_run_step_groups(text: str) -> list[list[tuple[str, bool | None]]]: uses = step.get("uses") run = step.get("run") if uses is not None: - # Action implementations are outside this local proof. Even a - # static external action can replace later command resolution - # through GITHUB_PATH, so no later test is proven reachable. - blocked = True - break + action = legacy.inline_scalar(uses[1][4]) + action_failure = _boolean_entry( + step.get("continue-on-error"), default=False + ) + if ( + run is not None + or action is None + or action not in trusted_actions + or action_failure is not False + ): + blocked = True + break + # The manifest may trust only immutable full-SHA action refs. + # This proves configured identity, not the action's behavior. + continue if run is None: continue shell = base._effective_entry( diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py index f49940cb..9faba700 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py @@ -3,6 +3,7 @@ from __future__ import annotations import re +import shlex from typing import Any, Mapping import lotus_family_workflow_policy as execution @@ -21,6 +22,9 @@ r"\$(?:\{[^}\n]*\}|[A-Za-z_][A-Za-z0-9_]*|\([^\n]*\)|[0-9@*#?$!_-])|" r"`|<\(|>\()" ) +_PINNED_ACTION = re.compile( + r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[0-9a-f]{40}$" +) _RUNNER_RESOLUTION_ENV_NAMES = { @@ -36,6 +40,15 @@ } +_TRUSTED_PREREQUISITE_COMMANDS = { + ("mix", "local.hex", "--force"), + ("mix", "local.rebar", "--force"), + ("mix", "deps.get"), + ("mix", "compile"), + ("mix", "format", "--check-formatted"), +} + + def _mutates_runner_state(script: str) -> bool: """Fail closed when setup can alter later command or environment meaning.""" visible = "\n".join( @@ -50,6 +63,25 @@ def _has_unproven_shell_expansion(command: str) -> bool: return bool(_SHELL_EXPANSION.search(visible)) +def _trusted_prerequisite_script(script: str) -> bool: + """Accept only closed literal setup forms needed by configured Mix CI.""" + commands: list[tuple[str, ...]] = [] + for line in script.splitlines(): + visible = execution.legacy.strip_comment(line).strip() + if not visible: + continue + if _has_unproven_shell_expansion(visible): + return False + try: + parts = tuple(shlex.split(visible)) + except ValueError: + return False + if parts not in _TRUSTED_PREREQUISITE_COMMANDS: + return False + commands.append(parts) + return bool(commands) + + def _command_matches( discovery: Mapping[str, Any], command: str ) -> list[str]: @@ -70,7 +102,19 @@ def _ci_discovery_one( ): return False, [] - groups = execution._github_run_step_groups(workflow_text) + trusted_actions = discovery.get("trusted_prerequisite_actions", []) + if not isinstance(trusted_actions, list) or any( + not isinstance(action, str) + or not _PINNED_ACTION.fullmatch(action) + for action in trusted_actions + ): + return False, [] + if len(trusted_actions) != len(set(trusted_actions)): + return False, [] + groups = execution._github_run_step_groups( + workflow_text, + trusted_actions, + ) scripts = [script for group in groups for script, _ in group] yaml_env_names = execution.legacy.yaml_env_names(workflow_text) if ( @@ -120,8 +164,11 @@ def _ci_discovery_one( break if continue_on_error is False and previous._proven_failure(command): break - # Ordinary static setup may precede a later gate. Dynamic runner-state - # changes, proven blockers, and unknown execution policy stop proof. + if continue_on_error is True and previous._proven_failure(command): + continue + if not _trusted_prerequisite_script(script): + break + # Only the closed prerequisite forms above may precede a later gate. continue return bool(matches), matches diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index 67099562..d09d1bdd 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -125,6 +125,25 @@ def test_expression_driven_job_failure_policy_is_rejected(self) -> None: (False, []), ) + def test_configured_pythia_workflow_remains_discoverable(self) -> None: + manifest = load_manifest(MANIFEST_PATH) + config = next( + row + for row in manifest["repositories"] + if row["id"] == "pythia" + ) + workflow_path = ROOT.parents[1] / config["ci_discovery"][ + "workflow_paths" + ][0] + + self.assertEqual( + ci_discovery( + config["ci_discovery"], + workflow_path.read_text(encoding="utf-8"), + ), + (True, ["mix test"]), + ) + class PytestConfigurationReviewHardeningTest(unittest.TestCase): """Bind default pytest discovery to hashed repository configuration.""" @@ -450,6 +469,40 @@ def test_plain_mix_configuration_is_hashed_and_keeps_pass(self) -> None: set(check["paths"]), {"mix.exs", "test/test_helper.exs"} ) + def test_ordinary_mix_module_and_required_file_are_hashed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + source_root = ROOT.parents[1] + for relative_path in ( + "mix.exs", + "mix/tasks/compile.rustler_fallback.ex", + ): + _write( + repository_root, + relative_path, + (source_root / relative_path).read_text(encoding="utf-8"), + ) + result = self._audit(snapshot_root) + + self.assertEqual(result["outcome"], PASS) + check = next( + row + for row in result["checks"] + if row["check_id"] == "mix_configuration" + ) + self.assertEqual(check["outcome"], PASS) + self.assertIn( + "mix/tasks/compile.rustler_fallback.ex", + set(check["paths"]), + ) + self.assertIn( + "mix/tasks/compile.rustler_fallback.ex", + {row["path"] for row in result["files"]}, + ) + if __name__ == "__main__": unittest.main() diff --git a/standards/lotus-family/conformance/test_policy_boundaries.py b/standards/lotus-family/conformance/test_policy_boundaries.py index 54441ed7..fcacf884 100644 --- a/standards/lotus-family/conformance/test_policy_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_boundaries.py @@ -149,6 +149,37 @@ def test_normal_setup_step_keeps_later_mix_test_discoverable(self) -> None: (True, ["mix test"]), ) + def test_workspace_writing_setup_blocks_later_test(self) -> None: + text = workflow_steps( + " - name: Replace audited runner input\n" + " run: printf 'raise SystemExit(0)\\n' > pytest.py\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + + def test_only_manifest_trusted_sha_pinned_action_is_accepted(self) -> None: + action = ( + "actions/checkout@" + "d23441a48e516b6c34aea4fa41551a30e30af803" + ) + text = workflow_steps( + " - name: Checkout\n" + f" uses: {action}\n" + " - name: Contract test\n" + " run: python -m pytest\n" + ) + trusted = copy.deepcopy(DISCOVERY) + trusted["trusted_prerequisite_actions"] = [action] + + self.assertEqual(ci_discovery(DISCOVERY, text), (False, [])) + self.assertEqual( + ci_discovery(trusted, text), + (True, ["python -m pytest"]), + ) + trusted["trusted_prerequisite_actions"] = ["actions/checkout@v6"] + self.assertEqual(ci_discovery(trusted, text), (False, [])) + def test_explicitly_ignored_predecessor_keeps_test_reachable(self) -> None: text = workflow_steps( " - name: Allowed failure\n" diff --git a/standards/lotus-family/manifest/lotus-family-v0.1.json b/standards/lotus-family/manifest/lotus-family-v0.1.json index 464f8f6f..fce70335 100644 --- a/standards/lotus-family/manifest/lotus-family-v0.1.json +++ b/standards/lotus-family/manifest/lotus-family-v0.1.json @@ -57,6 +57,12 @@ "workflow_paths": [ ".github/workflows/ci.yml" ], + "trusted_prerequisite_actions": [ + "actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803", + "erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124", + "dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c", + "actions/cache@caa296126883cff596d87d8935842f9db880ef25" + ], "strategy": "mix_default_discovery", "command": "mix test", "test_path": "test/lotus_docs_contract_test.exs" From c9d7bbb0f41c206cbeb9aa4c451c7d679b70bbce Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 05:47:20 +0700 Subject: [PATCH 74/79] fix: isolate Lotus Elixir contract execution --- .github/workflows/ci.yml | 3 + .../conformance/lotus_family_runtime_v2.py | 49 +++++-- .../lotus_family_workflow_policy_v4.py | 37 +++++- .../test_exact_head_review_hardening.py | 121 ++++++++++++++---- .../conformance/test_system_model.py | 10 +- .../manifest/lotus-family-v0.1.json | 7 +- 6 files changed, 180 insertions(+), 47 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2c89763..9a1c2507 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,9 @@ jobs: elixir-version: '1.16' otp-version: '26' + - name: Run Lotus docs contract without Mix project evaluation + run: elixir -e 'ExUnit.start()' test/lotus_docs_contract_test.exs -- + - name: Install Rust uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index 1662910f..0cbc2498 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -6,11 +6,12 @@ import json import os import re -import tomllib +from collections.abc import Mapping from pathlib import Path, PurePosixPath -from typing import Any, Mapping +from typing import Any import lotus_family_runtime as previous +import tomllib from lotus_family_schema import ( DRIFT, PASS, @@ -21,7 +22,6 @@ repository_config, ) - _PYTEST_CONFIG_PATHS = ( "pytest.toml", ".pytest.toml", @@ -60,6 +60,17 @@ _MIX_PROJECT_KEY = re.compile(r"([A-Za-z_][A-Za-z0-9_?!]*)\s*:") +_MIX_SAFE_PROJECT_VALUE = re.compile( + r"(?:" + r":[A-Za-z_][A-Za-z0-9_?!@]*|" + r'"(?:\\.|[^"\\])*"|' + r"'(?:\\.|[^'\\])*'|" + r"-?[0-9]+(?:\.[0-9]+)?|" + r"true|false|nil" + r")\Z" +) + + _MIX_REQUIRE_FILE = re.compile( r'Code\.require_file\("(?P[^"\\]+)",[ \t]*__DIR__\)' ) @@ -76,10 +87,7 @@ _MIX_COLLECTION_KEYS = {"test_paths", "test_pattern"} -_EXUNIT_SELECTION_SETTING = re.compile( - r"\bExUnit\.(?:start|configure)\s*\([^)]*\b(?:exclude|include)\s*:", - re.DOTALL, -) +_MIX_SAFE_TEST_HELPER = re.compile(r"ExUnit\.start\s*\(\s*\)") def _has_meaningful_lines(text: str) -> bool: @@ -323,8 +331,14 @@ def _mix_wrapper( if cursor != _skip_elixir_space_and_comments(text, definition_start): return None - suffix = text[definition_end:] - if not re.search(r"(?m)^\s*end\s*(?:#.*)?\s*\Z", suffix): + cursor = _skip_elixir_space_and_comments(text, definition_end) + end_match = re.match(r"end\b", text[cursor:]) + if end_match is None: + return None + cursor = _skip_elixir_space_and_comments( + text, cursor + end_match.end() + ) + if cursor != len(text): return None if len(required_files) != len(set(required_files)): return None @@ -376,7 +390,11 @@ def _literal_mix_project( match = _MIX_PROJECT_KEY.match(entry, entry_start) if match is None: return None - keys.add(match.group(1)) + key = match.group(1) + value = entry[match.end():].strip() + if key in keys or _MIX_SAFE_PROJECT_VALUE.fullmatch(value) is None: + return None + keys.add(key) return keys, required_files @@ -392,7 +410,11 @@ def _activates_mix_configuration(path: str, text: str) -> bool: project_keys = _literal_mix_project_keys(text) return project_keys is None or bool(project_keys & _MIX_COLLECTION_KEYS) if path == "test/test_helper.exs": - return bool(_EXUNIT_SELECTION_SETTING.search(text)) + cursor = _skip_elixir_space_and_comments(text, 0) + match = _MIX_SAFE_TEST_HELPER.match(text, cursor) + if match is None: + return True + return _skip_elixir_space_and_comments(text, match.end()) != len(text) return False @@ -427,8 +449,9 @@ def _audit_mix_configuration( if error is not None or text is None: blockers.append(f"{relative_path}: {error or 'unreadable'}") continue - if re.search(r"\b(?:test_paths|test_pattern)\b", text): - blockers.append(relative_path) + # Requiring an arbitrary Elixir file executes it while Mix loads the + # project. Hashing cannot prove that execution reaches the tests. + blockers.append(relative_path) return observed, blockers diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py index 9faba700..f674a70f 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py @@ -4,7 +4,8 @@ import re import shlex -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any import lotus_family_workflow_policy as execution import lotus_family_workflow_policy_v2 as policy_v2 @@ -31,6 +32,10 @@ "BASH_ENV", "ENV", "ERL_LIBS", + "ELIXIR_ERL_OPTIONS", + "ERL_AFLAGS", + "ERL_FLAGS", + "ERL_ZFLAGS", "MIX_ARCHIVES", "MIX_HOME", "MIX_PATH", @@ -40,6 +45,25 @@ } +def _uses_direct_elixir(discovery: Mapping[str, Any]) -> bool: + """Return true when a configured literal command invokes Elixir directly.""" + if discovery.get("strategy") != "contains_any": + return False + patterns = discovery.get("contains_any") + if not isinstance(patterns, list): + return False + for pattern in patterns: + if not isinstance(pattern, str): + continue + try: + parts = shlex.split(pattern) + except ValueError: + continue + if parts and parts[0] == "elixir": + return True + return False + + _TRUSTED_PREREQUISITE_COMMANDS = { ("mix", "local.hex", "--force"), ("mix", "local.rebar", "--force"), @@ -120,6 +144,13 @@ def _ci_discovery_one( if ( execution.legacy.UNRESOLVED_ENV_MAPPING in yaml_env_names or any(name in _RUNNER_RESOLUTION_ENV_NAMES for name in yaml_env_names) + or ( + _uses_direct_elixir(discovery) + and any( + name.startswith(("ERL_", "ELIXIR_")) + for name in yaml_env_names + ) + ) ): return False, [] if policy_v2._uses_pytest(discovery) and ( @@ -197,7 +228,7 @@ def ci_discovery( __all__ = [ "ci_discovery", "github_run_scripts", - "shell_commands", - "pytest_command", "mix_command", + "pytest_command", + "shell_commands", ] diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index d09d1bdd..4ffcedaa 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -2,6 +2,7 @@ from __future__ import annotations +import copy import tempfile import unittest from pathlib import Path @@ -9,7 +10,6 @@ from lotus_family_auditor import DRIFT, PASS, audit_repository, load_manifest from lotus_family_workflow import ci_discovery - HERE = Path(__file__).resolve().parent ROOT = HERE.parent MANIFEST_PATH = ROOT / "manifest" / "lotus-family-v0.1.json" @@ -141,7 +141,39 @@ def test_configured_pythia_workflow_remains_discoverable(self) -> None: config["ci_discovery"], workflow_path.read_text(encoding="utf-8"), ), - (True, ["mix test"]), + ( + True, + [ + ( + "elixir -e 'ExUnit.start()' " + "test/lotus_docs_contract_test.exs --" + ) + ], + ), + ) + + def test_direct_elixir_vm_flag_environment_is_rejected(self) -> None: + manifest = load_manifest(MANIFEST_PATH) + config = next( + row + for row in manifest["repositories"] + if row["id"] == "pythia" + ) + workflow = ( + "name: CI\n" + "env:\n" + " ERL_AFLAGS: -eval halt().\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - run: elixir -e 'ExUnit.start()' " + "test/lotus_docs_contract_test.exs --\n" + ) + + self.assertEqual( + ci_discovery(config["ci_discovery"], workflow), + (False, []), ) @@ -320,7 +352,18 @@ class MixConfigurationReviewHardeningTest(unittest.TestCase): """Bind Mix default discovery to hashed collection configuration.""" def setUp(self) -> None: - self.manifest = load_manifest(MANIFEST_PATH) + self.manifest = copy.deepcopy(load_manifest(MANIFEST_PATH)) + config = next( + row + for row in self.manifest["repositories"] + if row["id"] == "pythia" + ) + config["ci_discovery"] = { + "workflow_paths": [".github/workflows/ci.yml"], + "strategy": "mix_default_discovery", + "command": "mix test", + "test_path": "test/lotus_docs_contract_test.exs", + } def _audit(self, snapshot_root: Path) -> dict: return audit_repository( @@ -425,6 +468,41 @@ def test_executable_code_outside_mix_project_blocks_default_discovery(self) -> N self._assert_blocked_config(result, "mix.exs") + def test_executable_mix_project_value_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + _write( + repository_root, + "mix.exs", + "def project, do: [app: :lotus_fixture, version: System.halt(0)]\n", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "mix.exs") + + def test_executable_test_helper_blocks_default_discovery(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, self.manifest + ) + _write( + repository_root, + "mix.exs", + "def project, do: [app: :lotus_fixture]\n", + ) + _write( + repository_root, + "test/test_helper.exs", + "ExUnit.start()\nSystem.halt(0)\n", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, "test/test_helper.exs") + def test_exunit_selection_override_blocks_default_discovery(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) @@ -469,39 +547,36 @@ def test_plain_mix_configuration_is_hashed_and_keeps_pass(self) -> None: set(check["paths"]), {"mix.exs", "test/test_helper.exs"} ) - def test_ordinary_mix_module_and_required_file_are_hashed(self) -> None: + def test_required_mix_file_is_hashed_and_blocks(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) repository_root = _materialize_pythia( snapshot_root, self.manifest ) - source_root = ROOT.parents[1] - for relative_path in ( + _write( + repository_root, "mix.exs", - "mix/tasks/compile.rustler_fallback.ex", - ): - _write( - repository_root, - relative_path, - (source_root / relative_path).read_text(encoding="utf-8"), - ) + ( + 'Code.require_file("support.exs", __DIR__)\n' + "defmodule Fixture.MixProject do\n" + " use Mix.Project\n" + " def project, do: [app: :lotus_fixture]\n" + "end\n" + ), + ) + _write(repository_root, "support.exs", "System.halt(0)\n") result = self._audit(snapshot_root) - self.assertEqual(result["outcome"], PASS) + self.assertEqual(result["outcome"], DRIFT) check = next( row for row in result["checks"] if row["check_id"] == "mix_configuration" ) - self.assertEqual(check["outcome"], PASS) - self.assertIn( - "mix/tasks/compile.rustler_fallback.ex", - set(check["paths"]), - ) - self.assertIn( - "mix/tasks/compile.rustler_fallback.ex", - {row["path"] for row in result["files"]}, - ) + self.assertEqual(check["outcome"], DRIFT) + self.assertIn("support.exs", set(check["paths"])) + self.assertIn("support.exs", set(check["blocked_paths"])) + self.assertIn("support.exs", {row["path"] for row in result["files"]}) if __name__ == "__main__": diff --git a/standards/lotus-family/conformance/test_system_model.py b/standards/lotus-family/conformance/test_system_model.py index 8b976456..7442c499 100644 --- a/standards/lotus-family/conformance/test_system_model.py +++ b/standards/lotus-family/conformance/test_system_model.py @@ -242,11 +242,11 @@ def valid_command(discovery: dict) -> str: if discovery["strategy"] == "mix_default_discovery": return "mix test" pattern = discovery["contains_any"][0] - return ( - f"python -m pytest {pattern}" - if pattern.endswith(".py") - else f"mix test {pattern}" - ) + if pattern.endswith(".py"): + return f"python -m pytest {pattern}" + if pattern.endswith(".exs"): + return f"mix test {pattern}" + return pattern def materialize( diff --git a/standards/lotus-family/manifest/lotus-family-v0.1.json b/standards/lotus-family/manifest/lotus-family-v0.1.json index fce70335..b6a89967 100644 --- a/standards/lotus-family/manifest/lotus-family-v0.1.json +++ b/standards/lotus-family/manifest/lotus-family-v0.1.json @@ -63,9 +63,10 @@ "dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c", "actions/cache@caa296126883cff596d87d8935842f9db880ef25" ], - "strategy": "mix_default_discovery", - "command": "mix test", - "test_path": "test/lotus_docs_contract_test.exs" + "strategy": "contains_any", + "contains_any": [ + "elixir -e 'ExUnit.start()' test/lotus_docs_contract_test.exs --" + ] } }, { From c7a76da170d56e8f01cbcb4298c569a3c0fdf9d5 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 06:25:39 +0700 Subject: [PATCH 75/79] Pin explicit Lotus test source --- standards/lotus-family/ACCEPTANCE.md | 2 + .../lotus_family_auditor_legacy_tests.py | 9 ++ .../conformance/lotus_family_runtime.py | 22 +++- .../conformance/lotus_family_schema.py | 61 +++++++++- .../conformance/test_causality_model.py | 12 +- .../test_exact_head_review_hardening.py | 104 +++++++++++++++++- .../conformance/test_system_model.py | 9 ++ .../manifest/lotus-family-v0.1.json | 3 + 8 files changed, 207 insertions(+), 15 deletions(-) diff --git a/standards/lotus-family/ACCEPTANCE.md b/standards/lotus-family/ACCEPTANCE.md index f2dbc837..678a909d 100644 --- a/standards/lotus-family/ACCEPTANCE.md +++ b/standards/lotus-family/ACCEPTANCE.md @@ -2,6 +2,8 @@ - Removing a required English or Russian contract term produces `DRIFT`. - A regression test that is not discovered by configured CI produces `DRIFT`. +- Explicit discovery commands target their checked `test_path`; directly executed + Elixir test source must also match its manifest-pinned SHA-256. - Missing snapshots, refs, or exact commit identities produce `UNKNOWN`, never `PASS`. - Evidence records checked file paths and SHA-256 hashes. - Audit results remain advisory and grant no execution or merge authority. diff --git a/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py b/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py index 0a2cbd3d..6b521326 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py +++ b/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py @@ -43,6 +43,15 @@ def _materialize_repository(snapshot_root: Path, config: dict) -> Path: relative_path, "\n".join(dict.fromkeys(terms)) + "\n", ) + for check in config["file_checks"]: + if "sha256" in check: + _write( + repository_root, + check["path"], + (ROOT.parents[1] / check["path"]).read_text( + encoding="utf-8" + ), + ) discovery = config["ci_discovery"] _write( repository_root, diff --git a/standards/lotus-family/conformance/lotus_family_runtime.py b/standards/lotus-family/conformance/lotus_family_runtime.py index 7aeb5b5f..c54b282c 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime.py +++ b/standards/lotus-family/conformance/lotus_family_runtime.py @@ -142,18 +142,28 @@ def audit_repository( terms = [str(term) for term in check["contains_all"]] missing = [term for term in terms if term not in text] - check_outcome = PASS if not missing else DRIFT + expected_sha256 = check.get("sha256") + actual_sha256 = files[-1]["sha256"] + digest_matches = ( + expected_sha256 is None + or expected_sha256 == actual_sha256 + ) + check_outcome = PASS if not missing and digest_matches else DRIFT + if missing: + check_detail = "required terms are missing" + elif not digest_matches: + check_detail = "file SHA-256 does not match the pinned source" + elif expected_sha256 is not None: + check_detail = "all required terms and pinned digest match" + else: + check_detail = "all required terms are present" checks.append( { "check_id": check_id, "outcome": check_outcome, "path": name, "missing_terms": missing, - "detail": ( - "all required terms are present" - if check_outcome == PASS - else "required terms are missing" - ), + "detail": check_detail, } ) if check_outcome == DRIFT: diff --git a/standards/lotus-family/conformance/lotus_family_schema.py b/standards/lotus-family/conformance/lotus_family_schema.py index 74946fa3..bf010ddc 100644 --- a/standards/lotus-family/conformance/lotus_family_schema.py +++ b/standards/lotus-family/conformance/lotus_family_schema.py @@ -3,11 +3,13 @@ import hashlib import json import re +import shlex from pathlib import Path, PurePosixPath from typing import Any, Mapping PASS, DRIFT, UNKNOWN = "PASS", "DRIFT", "UNKNOWN" COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") +SHA256 = re.compile(r"^[0-9a-f]{64}$") ACTION_SHA_REF = re.compile( r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[0-9a-f]{40}$" ) @@ -36,6 +38,22 @@ def relative_path(value: Any, field: str) -> str: return path.as_posix() +def command_test_paths(value: Any, field: str) -> tuple[list[str], set[str]]: + """Parse one contains-any pattern and return its test-file targets.""" + text = non_empty(value, field) + try: + tokens = shlex.split(text) + except ValueError as exc: + raise ValueError(f"{field} must be valid shell words") from exc + + targets: set[str] = set() + for token in tokens: + candidate = token.split("::", 1)[0] + if candidate.lower().endswith((".py", ".exs")): + targets.add(relative_path(candidate, field)) + return tokens, targets + + def validate_manifest(manifest: Mapping[str, Any]) -> None: if manifest.get("schema_version") != "pythia.lotus_family_manifest.v0.1": raise ValueError("unsupported Lotus Family manifest schema") @@ -64,6 +82,7 @@ def validate_manifest(manifest: Mapping[str, Any]) -> None: raise ValueError(f"{prefix}.file_checks must be a non-empty list") check_ids: set[str] = set() checked: set[str] = set() + checks_by_path: dict[str, list[Mapping[str, Any]]] = {} for ci, check in enumerate(checks): cp = f"{prefix}.file_checks[{ci}]" if not isinstance(check, Mapping): @@ -72,7 +91,15 @@ def validate_manifest(manifest: Mapping[str, Any]) -> None: if check_id in check_ids: raise ValueError(f"duplicate check id in {repo_id}: {check_id}") check_ids.add(check_id) - checked.add(relative_path(check.get("path"), f"{cp}.path")) + check_path = relative_path(check.get("path"), f"{cp}.path") + checked.add(check_path) + checks_by_path.setdefault(check_path, []).append(check) + expected_sha256 = check.get("sha256") + if "sha256" in check and ( + not isinstance(expected_sha256, str) + or not SHA256.fullmatch(expected_sha256) + ): + raise ValueError(f"{cp}.sha256 must be a lowercase SHA-256") terms = check.get("contains_all") if not isinstance(terms, list) or not terms: raise ValueError(f"{cp}.contains_all must be a non-empty list") @@ -112,10 +139,36 @@ def validate_manifest(manifest: Mapping[str, Any]) -> None: patterns = discovery.get("contains_any") if not isinstance(patterns, list) or not patterns: raise ValueError(f"{prefix}.ci_discovery.contains_any must be a non-empty list") + test_path = relative_path( + discovery.get("test_path"), + f"{prefix}.ci_discovery.test_path", + ) + if not test_path.lower().endswith((".py", ".exs")): + raise ValueError( + f"{prefix}.ci_discovery.test_path must be a test source" + ) + if test_path not in checked: + raise ValueError( + f"{prefix}.ci_discovery.test_path must also be a checked file" + ) + direct_elixir = False for pi, pattern in enumerate(patterns): - text = non_empty(pattern, f"{prefix}.ci_discovery.contains_any[{pi}]") - if text.endswith((".py", ".exs")) and relative_path(text, f"{prefix}.ci_discovery.contains_any[{pi}]") not in checked: - raise ValueError(f"{prefix}.ci_discovery pattern must also be a checked file") + field = f"{prefix}.ci_discovery.contains_any[{pi}]" + tokens, targets = command_test_paths(pattern, field) + if targets != {test_path}: + raise ValueError( + f"{field} must target only ci_discovery.test_path" + ) + direct_elixir = direct_elixir or ( + bool(tokens) and tokens[0] == "elixir" + ) + if direct_elixir and not any( + "sha256" in check for check in checks_by_path[test_path] + ): + raise ValueError( + f"{prefix}.ci_discovery.test_path must pin sha256 " + "for direct Elixir execution" + ) elif strategy in {"pytest_default_discovery", "mix_default_discovery"}: expected = "python -m pytest" if strategy.startswith("pytest") else "mix test" if non_empty(discovery.get("command"), f"{prefix}.ci_discovery.command") != expected: diff --git a/standards/lotus-family/conformance/test_causality_model.py b/standards/lotus-family/conformance/test_causality_model.py index 6e32416c..63acb144 100644 --- a/standards/lotus-family/conformance/test_causality_model.py +++ b/standards/lotus-family/conformance/test_causality_model.py @@ -9,9 +9,6 @@ from pathlib import Path from lotus_family_auditor import ( - DRIFT, - PASS, - UNKNOWN, audit_repository, load_manifest, ) @@ -95,6 +92,15 @@ def _materialize( relative_path, "\n".join(dict.fromkeys(terms)) + "\n", ) + for check in config["file_checks"]: + if "sha256" in check: + _write( + repository_root, + check["path"], + (ROOT.parents[1] / check["path"]).read_text( + encoding="utf-8" + ), + ) discovery = config["ci_discovery"] _write( repository_root, diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index 4ffcedaa..d1560afa 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -8,6 +8,7 @@ from pathlib import Path from lotus_family_auditor import DRIFT, PASS, audit_repository, load_manifest +from lotus_family_schema import validate_manifest from lotus_family_workflow import ci_discovery HERE = Path(__file__).resolve().parent @@ -87,10 +88,24 @@ def _materialize_pythia(snapshot_root: Path, manifest: dict) -> Path: relative_path, "\n".join(dict.fromkeys(terms)) + "\n", ) + for check in config["file_checks"]: + if "sha256" in check: + _write( + repository_root, + check["path"], + (ROOT.parents[1] / check["path"]).read_text( + encoding="utf-8" + ), + ) + discovery = config["ci_discovery"] + if discovery["strategy"] == "mix_default_discovery": + command = discovery["command"] + else: + command = discovery["contains_any"][0] _write( repository_root, - config["ci_discovery"]["workflow_paths"][0], - _workflow().replace("python -m pytest", "mix test"), + discovery["workflow_paths"][0], + _workflow().replace("python -m pytest", command), ) return repository_root @@ -177,6 +192,91 @@ def test_direct_elixir_vm_flag_environment_is_rejected(self) -> None: ) +class ManifestSourceBindingReviewHardeningTest(unittest.TestCase): + """Bind explicit CI discovery to one immutable checked test source.""" + + def setUp(self) -> None: + self.manifest = copy.deepcopy(load_manifest(MANIFEST_PATH)) + self.config = next( + row + for row in self.manifest["repositories"] + if row["id"] == "pythia" + ) + + def test_contains_any_command_cannot_target_an_unchecked_test(self) -> None: + self.config["ci_discovery"]["contains_any"] = [ + "elixir -e 'ExUnit.start()' test/unrelated_test.exs --" + ] + + with self.assertRaisesRegex( + ValueError, + "must target only ci_discovery.test_path", + ): + validate_manifest(self.manifest) + + def test_direct_elixir_test_source_requires_a_pinned_digest(self) -> None: + for mutation in ("missing", "null"): + with self.subTest(mutation=mutation): + manifest = copy.deepcopy(self.manifest) + config = next( + row + for row in manifest["repositories"] + if row["id"] == "pythia" + ) + test_check = next( + check + for check in config["file_checks"] + if check["path"] + == "test/lotus_docs_contract_test.exs" + ) + if mutation == "missing": + del test_check["sha256"] + error = "must pin sha256 for direct Elixir execution" + else: + test_check["sha256"] = None + error = "sha256 must be a lowercase SHA-256" + + with self.assertRaisesRegex(ValueError, error): + validate_manifest(manifest) + + def test_self_disabling_direct_elixir_source_is_drift(self) -> None: + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_pythia( + snapshot_root, + self.manifest, + ) + _write( + repository_root, + "test/lotus_docs_contract_test.exs", + ( + "# PR evidence is bound to exact head and changed context\n" + "# English and Russian contracts preserve the " + "no-authority boundary\n" + "# Lotus remains a limitation contract rather than a " + "safety overclaim\n" + "System.halt(0)\n" + ), + ) + result = audit_repository( + self.manifest, + repository_id="pythia", + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + self.assertEqual(result["outcome"], DRIFT) + check = next( + row + for row in result["checks"] + if row["check_id"] == "regression_protection" + ) + self.assertEqual(check["outcome"], DRIFT) + self.assertEqual(check["missing_terms"], []) + self.assertIn("pinned source", check["detail"]) + + class PytestConfigurationReviewHardeningTest(unittest.TestCase): """Bind default pytest discovery to hashed repository configuration.""" diff --git a/standards/lotus-family/conformance/test_system_model.py b/standards/lotus-family/conformance/test_system_model.py index 7442c499..9598d5f7 100644 --- a/standards/lotus-family/conformance/test_system_model.py +++ b/standards/lotus-family/conformance/test_system_model.py @@ -266,6 +266,15 @@ def materialize( "\n".join(dict.fromkeys(values)) + "\n", encoding="utf-8", ) + for check in config["file_checks"]: + if "sha256" in check: + path = repository / check["path"] + path.write_text( + (ROOT.parents[1] / check["path"]).read_text( + encoding="utf-8" + ), + encoding="utf-8", + ) discovery = config["ci_discovery"] path = repository / discovery["workflow_paths"][0] path.parent.mkdir(parents=True, exist_ok=True) diff --git a/standards/lotus-family/manifest/lotus-family-v0.1.json b/standards/lotus-family/manifest/lotus-family-v0.1.json index b6a89967..ebc42328 100644 --- a/standards/lotus-family/manifest/lotus-family-v0.1.json +++ b/standards/lotus-family/manifest/lotus-family-v0.1.json @@ -46,6 +46,7 @@ { "id": "regression_protection", "path": "test/lotus_docs_contract_test.exs", + "sha256": "3bd2304e5aef290666adc6c58bdd731ca1194f77ab64ebbc19c04c7a263ceaaf", "contains_all": [ "PR evidence is bound to exact head and changed context", "English and Russian contracts preserve the no-authority boundary", @@ -64,6 +65,7 @@ "actions/cache@caa296126883cff596d87d8935842f9db880ef25" ], "strategy": "contains_any", + "test_path": "test/lotus_docs_contract_test.exs", "contains_any": [ "elixir -e 'ExUnit.start()' test/lotus_docs_contract_test.exs --" ] @@ -155,6 +157,7 @@ ".github/workflows/ci.yml" ], "strategy": "contains_any", + "test_path": "tests/test_lotus_docs_contract.py", "contains_any": [ "tests/test_lotus_docs_contract.py" ] From b4eded4d36dd06fcda278bbf934e6a2dadb55139 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 07:18:10 +0700 Subject: [PATCH 76/79] Bind Lotus test execution evidence --- standards/lotus-family/ACCEPTANCE.md | 7 +- standards/lotus-family/IMPLEMENTATION.md | 4 + standards/lotus-family/README.md | 6 + .../cml_test_lotus_docs_contract.py.fixture | 74 +++++++ .../ls_test_lotus_docs_contract.py.fixture | 65 ++++++ .../lotus_family_auditor_legacy_tests.py | 7 +- .../conformance/lotus_family_runtime_v2.py | 75 ++++++- .../conformance/lotus_family_schema.py | 22 +- .../conformance/lotus_family_test_sources.py | 34 +++ .../conformance/test_causality_model.py | 7 +- .../test_exact_head_review_hardening.py | 196 +++++++++++++++++- .../test_explicit_pytest_configuration.py | 12 ++ .../conformance/test_system_model.py | 7 +- .../manifest/lotus-family-v0.1.json | 2 + 14 files changed, 489 insertions(+), 29 deletions(-) create mode 100644 standards/lotus-family/conformance/fixtures/cml_test_lotus_docs_contract.py.fixture create mode 100644 standards/lotus-family/conformance/fixtures/ls_test_lotus_docs_contract.py.fixture create mode 100644 standards/lotus-family/conformance/lotus_family_test_sources.py diff --git a/standards/lotus-family/ACCEPTANCE.md b/standards/lotus-family/ACCEPTANCE.md index 678a909d..1bfd69e5 100644 --- a/standards/lotus-family/ACCEPTANCE.md +++ b/standards/lotus-family/ACCEPTANCE.md @@ -2,8 +2,11 @@ - Removing a required English or Russian contract term produces `DRIFT`. - A regression test that is not discovered by configured CI produces `DRIFT`. -- Explicit discovery commands target their checked `test_path`; directly executed - Elixir test source must also match its manifest-pinned SHA-256. +- Explicit discovery commands target their checked `test_path`; every executed + Python or Elixir test source must match its manifest-pinned SHA-256. +- Repository-local pytest modules, packages, sourceless bytecode, native + extensions, and Python startup hooks that can preempt collection produce + `DRIFT`. - Missing snapshots, refs, or exact commit identities produce `UNKNOWN`, never `PASS`. - Evidence records checked file paths and SHA-256 hashes. - Audit results remain advisory and grant no execution or merge authority. diff --git a/standards/lotus-family/IMPLEMENTATION.md b/standards/lotus-family/IMPLEMENTATION.md index 174aee19..75385bb7 100644 --- a/standards/lotus-family/IMPLEMENTATION.md +++ b/standards/lotus-family/IMPLEMENTATION.md @@ -3,3 +3,7 @@ This first slice turns the Lotus Family strength map into executable checks. It intentionally audits materialized exact snapshots and does not perform remote repository access or consequential actions. + +The runtime binds every configured executable test source to its manifest +digest and rejects local interpreter/import shadows that can terminate pytest +without collecting that source. diff --git a/standards/lotus-family/README.md b/standards/lotus-family/README.md index 3915be6c..156b1752 100644 --- a/standards/lotus-family/README.md +++ b/standards/lotus-family/README.md @@ -40,6 +40,12 @@ a structural reachability assumption, not proof of side-effect freedom; arbitrary commands or direct writes to audited inputs never establish a later test gate. +The configured Python and Elixir contract-test sources must match pinned +SHA-256 values, so retaining required phrases in a replacement no-op test cannot +produce `PASS`. Pytest discovery also fails closed on repository-local source, +sourceless bytecode, native-extension, or Python-startup shadows that could +preempt the installed runner before contract collection. + ## Causal spacetime testing model The test model has two compatible layers: diff --git a/standards/lotus-family/conformance/fixtures/cml_test_lotus_docs_contract.py.fixture b/standards/lotus-family/conformance/fixtures/cml_test_lotus_docs_contract.py.fixture new file mode 100644 index 00000000..81104400 --- /dev/null +++ b/standards/lotus-family/conformance/fixtures/cml_test_lotus_docs_contract.py.fixture @@ -0,0 +1,74 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +LOTUS = ROOT / "docs" / "LOTUS.md" +PR_TEMPLATE = ROOT / ".github" / "pull_request_template.md" + + +def test_pr_evidence_is_bound_to_exact_head() -> None: + template = PR_TEMPLATE.read_text(encoding="utf-8") + + assert "Exact PR head SHA validated" in template + assert "Validation command" in template + assert "Validation was run or rerun after the most recent head change" in template + assert "Evidence becomes stale" in template + assert "when the PR head changes" in template + + +def test_supersession_requires_identity_scope_and_review() -> None: + text = LOTUS.read_text(encoding="utf-8") + + required_english = ( + "same canonical memory identity", + "compatible source", + "repository scope", + "recorded as a conflict", + "cannot silently replace", + ) + for phrase in required_english: + assert phrase in text + + assert "repository B / same topic but different identity or scope" in text + assert "may not silently supersede" in text + + +def test_english_and_russian_contracts_keep_core_acceptance_boundaries() -> None: + text = LOTUS.read_text(encoding="utf-8") + english, russian = text.split("# Слой Лотоса CML", maxsplit=1) + + english_terms = ( + "bounded evidence", + "schema validation", + "canonical identity", + "explicit review", + "source, scope, state, and time", + "supersede", + "reject", + ) + for term in english_terms: + assert term in english + + russian_terms = ( + "ограниченного набора доказательств", + "проверки схемы", + "канонической идентичности", + "явного review", + "источнику, scope, состоянию и времени", + "заменить", + "отклонить", + ) + for term in russian_terms: + assert term in russian + + +def test_english_and_russian_contracts_keep_the_no_authority_boundary() -> None: + text = LOTUS.read_text(encoding="utf-8") + english, russian = text.split("# Слой Лотоса CML", maxsplit=1) + + assert "has no ownership, approval, execution, delivery, or merge authority" in english + assert "не имеет права собственности" in russian + assert "одобрения" in russian + assert "исполнения" in russian + assert "доставки или merge" in russian + assert "не скрытый policy engine" in russian diff --git a/standards/lotus-family/conformance/fixtures/ls_test_lotus_docs_contract.py.fixture b/standards/lotus-family/conformance/fixtures/ls_test_lotus_docs_contract.py.fixture new file mode 100644 index 00000000..abf1709b --- /dev/null +++ b/standards/lotus-family/conformance/fixtures/ls_test_lotus_docs_contract.py.fixture @@ -0,0 +1,65 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +LOTUS = ROOT / "LOTUS.md" +PR_TEMPLATE = ROOT / ".github" / "pull_request_template.md" + + +def test_pr_evidence_is_bound_to_exact_head() -> None: + template = PR_TEMPLATE.read_text(encoding="utf-8") + + assert "Exact PR head SHA validated" in template + assert "Validation command" in template + assert "Validation was run or rerun after the most recent PR head change" in template + assert "Evidence becomes stale" in template + assert "apply to the exact SHA above" in template + + +def test_lotus_preserves_human_authority_in_both_languages() -> None: + text = LOTUS.read_text(encoding="utf-8") + english, russian = text.split("# Слой Лотоса", maxsplit=1) + + assert "has no ownership, approval, execution, delivery, or merge authority" in english + assert "не имеет права собственности" in russian + assert "одобрения" in russian + assert "исполнения" in russian + assert "доставки или merge" in russian + + +def test_english_and_russian_contracts_keep_the_seven_core_petals() -> None: + text = LOTUS.read_text(encoding="utf-8") + english, russian = text.split("# Слой Лотоса", maxsplit=1) + + english_petals = ( + "Clarity from complexity", + "Evidence before confidence", + "Causes before symptoms", + "Memory without authority", + "Consent before durable memory", + "Repair before judgment", + "Human authorship at the center", + ) + russian_petals = ( + "Ясность из сложности", + "Доказательства до уверенности", + "Причины до симптомов", + "Память без власти", + "Согласие до долговременной памяти", + "Исправление до осуждения", + "Человек остаётся автором", + ) + + for phrase in english_petals: + assert phrase in english + for phrase in russian_petals: + assert phrase in russian + + +def test_lotus_remains_guidance_not_runtime_authority() -> None: + text = LOTUS.read_text(encoding="utf-8") + + assert "not a runtime component, a permission system, or an autonomous actor" in text + assert "not a personality cult, hidden authority, mystical proof" in text + assert "не runtime-компонент, не система разрешений и не автономный агент" in text + assert "не культ личности, не скрытая власть, не мистическое доказательство" in text diff --git a/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py b/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py index 6b521326..e5d4ce14 100644 --- a/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py +++ b/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py @@ -6,6 +6,7 @@ from pathlib import Path from lotus_family_auditor import DRIFT, PASS, UNKNOWN, audit_repository, load_manifest +from lotus_family_test_sources import pinned_test_source HERE = Path(__file__).resolve().parent ROOT = HERE.parent @@ -48,8 +49,10 @@ def _materialize_repository(snapshot_root: Path, config: dict) -> Path: _write( repository_root, check["path"], - (ROOT.parents[1] / check["path"]).read_text( - encoding="utf-8" + pinned_test_source( + config["id"], + check["path"], + check["sha256"], ), ) discovery = config["ci_discovery"] diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index 0cbc2498..a5d035e0 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -34,9 +34,21 @@ _PYTEST_SHADOW_PATHS = ( + "pytest", "pytest.py", + "pytest.pyc", "pytest/__init__.py", + "pytest/__init__.pyc", "pytest/__main__.py", + "pytest/__main__.pyc", +) + + +_PYTHON_STARTUP_SHADOW_PATHS = ( + "sitecustomize.py", + "sitecustomize.pyc", + "usercustomize.py", + "usercustomize.pyc", ) @@ -90,6 +102,62 @@ _MIX_SAFE_TEST_HELPER = re.compile(r"ExUnit\.start\s*\(\s*\)") +def _looks_like_import_shadow(name: str, stems: tuple[str, ...]) -> bool: + """Recognize sourceless bytecode and native extension module names.""" + return any( + name.startswith(f"{stem}.") + and name.lower().endswith((".pyc", ".so", ".pyd")) + for stem in stems + ) + + +def _discover_import_shadow_paths(repository_root: Path) -> list[str]: + """Find interpreter-specific module shadows without following symlinks.""" + discovered: list[str] = [] + try: + root_entries = list(repository_root.iterdir()) + except OSError: + return discovered + for candidate in root_entries: + if _looks_like_import_shadow( + candidate.name, + ("pytest", "sitecustomize", "usercustomize"), + ): + discovered.append(candidate.name) + + package = repository_root / "pytest" + if package.is_dir() and not package.is_symlink(): + try: + package_entries = list(package.iterdir()) + except OSError: + package_entries = [] + for candidate in package_entries: + if _looks_like_import_shadow( + candidate.name, + ("__init__", "__main__"), + ): + discovered.append(f"pytest/{candidate.name}") + return sorted(set(discovered)) + + +def _is_import_shadow_path(path: str) -> bool: + """Return true for repository files that can preempt pytest execution.""" + if path in {*_PYTEST_SHADOW_PATHS, *_PYTHON_STARTUP_SHADOW_PATHS}: + return True + candidate = PurePosixPath(path) + if candidate.parent == PurePosixPath("."): + return _looks_like_import_shadow( + candidate.name, + ("pytest", "sitecustomize", "usercustomize"), + ) + return candidate.parent == PurePosixPath("pytest") and ( + _looks_like_import_shadow( + candidate.name, + ("__init__", "__main__"), + ) + ) + + def _has_meaningful_lines(text: str) -> bool: """Return true when a dedicated pytest config is not empty/comment-only.""" return any( @@ -149,7 +217,7 @@ def _requires_mix_configuration_audit( def _activates_pytest_configuration(path: str, text: str) -> bool: """Detect config scopes or hooks that can alter default pytest collection.""" - if path in _PYTEST_SHADOW_PATHS: + if _is_import_shadow_path(path): return True if _is_conftest(path): return True @@ -208,6 +276,8 @@ def _audit_pytest_configuration( candidate_paths = ( *_PYTEST_CONFIG_PATHS, *_PYTEST_SHADOW_PATHS, + *_PYTHON_STARTUP_SHADOW_PATHS, + *_discover_import_shadow_paths(repository_root), *_discover_conftest_paths(repository_root), ) for relative_path in dict.fromkeys(candidate_paths): @@ -216,6 +286,9 @@ def _audit_pytest_configuration( continue observed.append(relative_path) text, error = read_file(repository_root, relative_path, files) + if _is_import_shadow_path(relative_path): + blockers.append(relative_path) + continue if error is not None or text is None: blockers.append(f"{relative_path}: {error or 'unreadable'}") continue diff --git a/standards/lotus-family/conformance/lotus_family_schema.py b/standards/lotus-family/conformance/lotus_family_schema.py index bf010ddc..c70aa30a 100644 --- a/standards/lotus-family/conformance/lotus_family_schema.py +++ b/standards/lotus-family/conformance/lotus_family_schema.py @@ -151,24 +151,13 @@ def validate_manifest(manifest: Mapping[str, Any]) -> None: raise ValueError( f"{prefix}.ci_discovery.test_path must also be a checked file" ) - direct_elixir = False for pi, pattern in enumerate(patterns): field = f"{prefix}.ci_discovery.contains_any[{pi}]" - tokens, targets = command_test_paths(pattern, field) + _, targets = command_test_paths(pattern, field) if targets != {test_path}: raise ValueError( f"{field} must target only ci_discovery.test_path" ) - direct_elixir = direct_elixir or ( - bool(tokens) and tokens[0] == "elixir" - ) - if direct_elixir and not any( - "sha256" in check for check in checks_by_path[test_path] - ): - raise ValueError( - f"{prefix}.ci_discovery.test_path must pin sha256 " - "for direct Elixir execution" - ) elif strategy in {"pytest_default_discovery", "mix_default_discovery"}: expected = "python -m pytest" if strategy.startswith("pytest") else "mix test" if non_empty(discovery.get("command"), f"{prefix}.ci_discovery.command") != expected: @@ -183,6 +172,13 @@ def validate_manifest(manifest: Mapping[str, Any]) -> None: raise ValueError(f"{prefix}.ci_discovery.test_path must also be a checked file") else: raise ValueError(f"{prefix}.ci_discovery.strategy is unsupported: {strategy}") + if not any( + "sha256" in check for check in checks_by_path[test_path] + ): + raise ValueError( + f"{prefix}.ci_discovery.test_path must pin sha256 " + "for executed test source" + ) def load_manifest(path: Path) -> dict[str, Any]: @@ -214,8 +210,8 @@ def read_file(root: Path, name: str, files: list[dict[str, str]]) -> tuple[str | return None, f"required file is missing: {name}" try: data = path.read_bytes() - text = data.decode("utf-8") files.append({"path": name, "sha256": hashlib.sha256(data).hexdigest()}) + text = data.decode("utf-8") return text, None except (OSError, UnicodeError) as exc: return None, f"cannot read {name}: {exc.__class__.__name__}" diff --git a/standards/lotus-family/conformance/lotus_family_test_sources.py b/standards/lotus-family/conformance/lotus_family_test_sources.py new file mode 100644 index 00000000..6085d5e8 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_test_sources.py @@ -0,0 +1,34 @@ +"""Pinned external test sources used by synthetic conformance snapshots.""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPOSITORY_ROOT = HERE.parents[2] + +_EXTERNAL_FIXTURES = { + "cml": HERE / "fixtures" / "cml_test_lotus_docs_contract.py.fixture", + "ls": HERE / "fixtures" / "ls_test_lotus_docs_contract.py.fixture", +} + + +def pinned_test_source( + repository_id: str, + relative_path: str, + expected_sha256: str, +) -> str: + """Load one canonical source fixture and verify its manifest digest.""" + if repository_id == "pythia": + path = REPOSITORY_ROOT / relative_path + else: + path = _EXTERNAL_FIXTURES[repository_id] + data = path.read_bytes() + actual_sha256 = hashlib.sha256(data).hexdigest() + if actual_sha256 != expected_sha256: + raise AssertionError( + f"pinned source fixture drift: {repository_id}: " + f"{actual_sha256} != {expected_sha256}" + ) + return data.decode("utf-8") diff --git a/standards/lotus-family/conformance/test_causality_model.py b/standards/lotus-family/conformance/test_causality_model.py index 63acb144..65554f61 100644 --- a/standards/lotus-family/conformance/test_causality_model.py +++ b/standards/lotus-family/conformance/test_causality_model.py @@ -12,6 +12,7 @@ audit_repository, load_manifest, ) +from lotus_family_test_sources import pinned_test_source HERE = Path(__file__).resolve().parent ROOT = HERE.parent @@ -97,8 +98,10 @@ def _materialize( _write( repository_root, check["path"], - (ROOT.parents[1] / check["path"]).read_text( - encoding="utf-8" + pinned_test_source( + config["id"], + check["path"], + check["sha256"], ), ) discovery = config["ci_discovery"] diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index d1560afa..e5ce7b82 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -3,12 +3,14 @@ from __future__ import annotations import copy +import py_compile import tempfile import unittest from pathlib import Path from lotus_family_auditor import DRIFT, PASS, audit_repository, load_manifest from lotus_family_schema import validate_manifest +from lotus_family_test_sources import pinned_test_source from lotus_family_workflow import ci_discovery HERE = Path(__file__).resolve().parent @@ -48,9 +50,30 @@ def _write(root: Path, relative_path: str, content: str) -> None: path.write_text(content, encoding="utf-8") -def _materialize_cml(snapshot_root: Path, manifest: dict) -> Path: +def _write_bytes(root: Path, relative_path: str, content: bytes) -> None: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + + +def _compile_sourceless(root: Path, relative_path: str) -> None: + target = root / relative_path + source = target.with_name(f"{target.stem}.source.py") + source.parent.mkdir(parents=True, exist_ok=True) + source.write_text("raise SystemExit(0)\n", encoding="utf-8") + py_compile.compile(str(source), cfile=str(target), doraise=True) + source.unlink() + + +def _materialize_python_repository( + snapshot_root: Path, + manifest: dict, + repository_id: str, +) -> Path: config = next( - row for row in manifest["repositories"] if row["id"] == "cml" + row + for row in manifest["repositories"] + if row["id"] == repository_id ) repository_root = snapshot_root / config["snapshot_dir"] terms_by_path: dict[str, list[str]] = {} @@ -64,14 +87,39 @@ def _materialize_cml(snapshot_root: Path, manifest: dict) -> Path: relative_path, "\n".join(dict.fromkeys(terms)) + "\n", ) + for check in config["file_checks"]: + if "sha256" in check: + _write( + repository_root, + check["path"], + pinned_test_source( + config["id"], + check["path"], + check["sha256"], + ), + ) + discovery = config["ci_discovery"] + command = ( + discovery["command"] + if discovery["strategy"] == "pytest_default_discovery" + else f"python -m pytest {discovery['test_path']}" + ) _write( repository_root, - config["ci_discovery"]["workflow_paths"][0], - _workflow(), + discovery["workflow_paths"][0], + _workflow().replace("python -m pytest", command), ) return repository_root +def _materialize_cml(snapshot_root: Path, manifest: dict) -> Path: + return _materialize_python_repository( + snapshot_root, + manifest, + "cml", + ) + + def _materialize_pythia(snapshot_root: Path, manifest: dict) -> Path: config = next( row for row in manifest["repositories"] if row["id"] == "pythia" @@ -93,8 +141,10 @@ def _materialize_pythia(snapshot_root: Path, manifest: dict) -> Path: _write( repository_root, check["path"], - (ROOT.parents[1] / check["path"]).read_text( - encoding="utf-8" + pinned_test_source( + config["id"], + check["path"], + check["sha256"], ), ) discovery = config["ci_discovery"] @@ -231,7 +281,7 @@ def test_direct_elixir_test_source_requires_a_pinned_digest(self) -> None: ) if mutation == "missing": del test_check["sha256"] - error = "must pin sha256 for direct Elixir execution" + error = "must pin sha256 for executed test source" else: test_check["sha256"] = None error = "sha256 must be a lowercase SHA-256" @@ -239,6 +289,76 @@ def test_direct_elixir_test_source_requires_a_pinned_digest(self) -> None: with self.assertRaisesRegex(ValueError, error): validate_manifest(manifest) + def test_pytest_test_sources_require_pinned_digests(self) -> None: + for repository_id in ("cml", "ls"): + with self.subTest(repository_id=repository_id): + manifest = copy.deepcopy(self.manifest) + config = next( + row + for row in manifest["repositories"] + if row["id"] == repository_id + ) + test_check = next( + check + for check in config["file_checks"] + if check["path"] == config["ci_discovery"]["test_path"] + ) + del test_check["sha256"] + + with self.assertRaisesRegex( + ValueError, + "must pin sha256 for executed test source", + ): + validate_manifest(manifest) + + def test_phrase_preserving_pytest_source_replacement_is_drift(self) -> None: + for repository_id in ("cml", "ls"): + with self.subTest(repository_id=repository_id): + manifest = copy.deepcopy(self.manifest) + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_python_repository( + snapshot_root, + manifest, + repository_id, + ) + config = next( + row + for row in manifest["repositories"] + if row["id"] == repository_id + ) + test_check = next( + check + for check in config["file_checks"] + if check["path"] + == config["ci_discovery"]["test_path"] + ) + _write( + repository_root, + test_check["path"], + "\n".join( + f"# {term}" + for term in test_check["contains_all"] + ) + + "\n\ndef test_trivial_pass() -> None:\n pass\n", + ) + result = audit_repository( + manifest, + repository_id=repository_id, + snapshot_root=snapshot_root, + repository_ref="refs/heads/main", + commit_sha=SHA, + ) + + self.assertEqual(result["outcome"], DRIFT) + check = next( + row + for row in result["checks"] + if row["check_id"] == "regression_protection" + ) + self.assertEqual(check["missing_terms"], []) + self.assertIn("pinned source", check["detail"]) + def test_self_disabling_direct_elixir_source_is_drift(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) @@ -424,6 +544,68 @@ def test_repository_pytest_shadows_block_default_discovery(self) -> None: self._assert_blocked_config(result, relative_path) + def test_sourceless_pytest_bytecode_shadows_block_discovery(self) -> None: + cases = ( + "pytest.pyc", + "pytest/__init__.pyc", + "pytest/__main__.pyc", + ) + for relative_path in cases: + with self.subTest(relative_path=relative_path): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml( + snapshot_root, + self.manifest, + ) + _compile_sourceless(repository_root, relative_path) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, relative_path) + + def test_native_pytest_extension_shadows_block_discovery(self) -> None: + cases = ( + "pytest.cpython-312-x86_64-linux-gnu.so", + "pytest/__main__.pyd", + ) + for relative_path in cases: + with self.subTest(relative_path=relative_path): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml( + snapshot_root, + self.manifest, + ) + _write_bytes(repository_root, relative_path, b"\xffshadow") + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, relative_path) + + def test_python_startup_shadows_block_pytest_discovery(self) -> None: + cases = ( + "sitecustomize.pyc", + "usercustomize.cpython-312-x86_64-linux-gnu.so", + ) + for relative_path in cases: + with self.subTest(relative_path=relative_path): + with tempfile.TemporaryDirectory() as directory: + snapshot_root = Path(directory) + repository_root = _materialize_cml( + snapshot_root, + self.manifest, + ) + if relative_path.endswith(".pyc"): + _compile_sourceless(repository_root, relative_path) + else: + _write_bytes( + repository_root, + relative_path, + b"\xffshadow", + ) + result = self._audit(snapshot_root) + + self._assert_blocked_config(result, relative_path) + def test_non_pytest_pyproject_scope_is_hashed_and_keeps_pass(self) -> None: with tempfile.TemporaryDirectory() as directory: snapshot_root = Path(directory) diff --git a/standards/lotus-family/conformance/test_explicit_pytest_configuration.py b/standards/lotus-family/conformance/test_explicit_pytest_configuration.py index 77b36903..de1b69f4 100644 --- a/standards/lotus-family/conformance/test_explicit_pytest_configuration.py +++ b/standards/lotus-family/conformance/test_explicit_pytest_configuration.py @@ -7,6 +7,7 @@ from pathlib import Path from lotus_family_auditor import DRIFT, audit_repository, load_manifest +from lotus_family_test_sources import pinned_test_source HERE = Path(__file__).resolve().parent @@ -37,6 +38,17 @@ def _materialize_ls(snapshot_root: Path, manifest: dict) -> Path: relative_path, "\n".join(dict.fromkeys(terms)) + "\n", ) + for check in config["file_checks"]: + if "sha256" in check: + _write( + repository_root, + check["path"], + pinned_test_source( + config["id"], + check["path"], + check["sha256"], + ), + ) _write( repository_root, config["ci_discovery"]["workflow_paths"][0], diff --git a/standards/lotus-family/conformance/test_system_model.py b/standards/lotus-family/conformance/test_system_model.py index 9598d5f7..9a43d118 100644 --- a/standards/lotus-family/conformance/test_system_model.py +++ b/standards/lotus-family/conformance/test_system_model.py @@ -21,6 +21,7 @@ validate_graph, validate_routes, ) +from lotus_family_test_sources import pinned_test_source HERE = Path(__file__).resolve().parent ROOT = HERE.parent @@ -270,8 +271,10 @@ def materialize( if "sha256" in check: path = repository / check["path"] path.write_text( - (ROOT.parents[1] / check["path"]).read_text( - encoding="utf-8" + pinned_test_source( + config["id"], + check["path"], + check["sha256"], ), encoding="utf-8", ) diff --git a/standards/lotus-family/manifest/lotus-family-v0.1.json b/standards/lotus-family/manifest/lotus-family-v0.1.json index ebc42328..f3f5e1b5 100644 --- a/standards/lotus-family/manifest/lotus-family-v0.1.json +++ b/standards/lotus-family/manifest/lotus-family-v0.1.json @@ -99,6 +99,7 @@ { "id": "regression_protection", "path": "tests/test_lotus_docs_contract.py", + "sha256": "69de6a265928685d814d7bd1d77719a090a45ed4e9eecee9677c2d2e8259e5df", "contains_all": [ "test_english_and_russian_contracts_keep_the_no_authority_boundary", "has no ownership, approval, execution, delivery, or merge authority", @@ -145,6 +146,7 @@ { "id": "regression_protection", "path": "tests/test_lotus_docs_contract.py", + "sha256": "86662af527b02e0d950feaba66819ddfdd3dcccbb8b10b125bf8085e259d4589", "contains_all": [ "test_pr_evidence_is_bound_to_exact_head", "test_lotus_preserves_human_authority_in_both_languages", From 8031324e1be988f6f8ae79b3628ac90396d46b83 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 07:34:51 +0700 Subject: [PATCH 77/79] Block pytest dependency shadows --- standards/lotus-family/ACCEPTANCE.md | 6 +- standards/lotus-family/README.md | 6 +- .../conformance/lotus_family_runtime_v2.py | 58 ++++++++++++++----- .../test_exact_head_review_hardening.py | 13 +++++ 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/standards/lotus-family/ACCEPTANCE.md b/standards/lotus-family/ACCEPTANCE.md index 1bfd69e5..d331f0f8 100644 --- a/standards/lotus-family/ACCEPTANCE.md +++ b/standards/lotus-family/ACCEPTANCE.md @@ -4,9 +4,9 @@ - A regression test that is not discovered by configured CI produces `DRIFT`. - Explicit discovery commands target their checked `test_path`; every executed Python or Elixir test source must match its manifest-pinned SHA-256. -- Repository-local pytest modules, packages, sourceless bytecode, native - extensions, and Python startup hooks that can preempt collection produce - `DRIFT`. +- Repository-local pytest or pytest-dependency modules and packages, + sourceless bytecode, native extensions, and Python startup hooks that can + preempt collection produce `DRIFT`. - Missing snapshots, refs, or exact commit identities produce `UNKNOWN`, never `PASS`. - Evidence records checked file paths and SHA-256 hashes. - Audit results remain advisory and grant no execution or merge authority. diff --git a/standards/lotus-family/README.md b/standards/lotus-family/README.md index 156b1752..a3a6ebe8 100644 --- a/standards/lotus-family/README.md +++ b/standards/lotus-family/README.md @@ -42,9 +42,9 @@ test gate. The configured Python and Elixir contract-test sources must match pinned SHA-256 values, so retaining required phrases in a replacement no-op test cannot -produce `PASS`. Pytest discovery also fails closed on repository-local source, -sourceless bytecode, native-extension, or Python-startup shadows that could -preempt the installed runner before contract collection. +produce `PASS`. Pytest discovery also fails closed on repository-local pytest, +pytest-dependency, sourceless-bytecode, native-extension, or Python-startup +shadows that could preempt the installed runner before contract collection. ## Causal spacetime testing model diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index a5d035e0..fbe308c5 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -33,12 +33,37 @@ ) -_PYTEST_SHADOW_PATHS = ( +_PYTEST_IMPORT_MODULES = ( "pytest", - "pytest.py", - "pytest.pyc", - "pytest/__init__.py", - "pytest/__init__.pyc", + "_pytest", + "pluggy", + "iniconfig", + "packaging", + "pygments", + "colorama", + "tomli", + "exceptiongroup", +) + + +_PYTEST_PACKAGE_ENTRY_STEMS = { + module: ("__init__",) + for module in _PYTEST_IMPORT_MODULES +} +_PYTEST_PACKAGE_ENTRY_STEMS["pytest"] = ("__init__", "__main__") + + +_PYTEST_SHADOW_PATHS = tuple( + path + for module in _PYTEST_IMPORT_MODULES + for path in ( + module, + f"{module}.py", + f"{module}.pyc", + f"{module}/__init__.py", + f"{module}/__init__.pyc", + ) +) + ( "pytest/__main__.py", "pytest/__main__.pyc", ) @@ -121,12 +146,14 @@ def _discover_import_shadow_paths(repository_root: Path) -> list[str]: for candidate in root_entries: if _looks_like_import_shadow( candidate.name, - ("pytest", "sitecustomize", "usercustomize"), + (*_PYTEST_IMPORT_MODULES, "sitecustomize", "usercustomize"), ): discovered.append(candidate.name) - package = repository_root / "pytest" - if package.is_dir() and not package.is_symlink(): + for package_name, entry_stems in _PYTEST_PACKAGE_ENTRY_STEMS.items(): + package = repository_root / package_name + if not package.is_dir() or package.is_symlink(): + continue try: package_entries = list(package.iterdir()) except OSError: @@ -134,9 +161,9 @@ def _discover_import_shadow_paths(repository_root: Path) -> list[str]: for candidate in package_entries: if _looks_like_import_shadow( candidate.name, - ("__init__", "__main__"), + entry_stems, ): - discovered.append(f"pytest/{candidate.name}") + discovered.append(f"{package_name}/{candidate.name}") return sorted(set(discovered)) @@ -148,13 +175,12 @@ def _is_import_shadow_path(path: str) -> bool: if candidate.parent == PurePosixPath("."): return _looks_like_import_shadow( candidate.name, - ("pytest", "sitecustomize", "usercustomize"), - ) - return candidate.parent == PurePosixPath("pytest") and ( - _looks_like_import_shadow( - candidate.name, - ("__init__", "__main__"), + (*_PYTEST_IMPORT_MODULES, "sitecustomize", "usercustomize"), ) + entry_stems = _PYTEST_PACKAGE_ENTRY_STEMS.get(str(candidate.parent)) + return entry_stems is not None and _looks_like_import_shadow( + candidate.name, + entry_stems, ) diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index e5ce7b82..4f2be52f 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -530,6 +530,14 @@ def test_repository_pytest_shadows_block_default_discovery(self) -> None: "pytest.py", "pytest/__init__.py", "pytest/__main__.py", + "pluggy.py", + "iniconfig.py", + "_pytest/__init__.py", + "packaging.py", + "pygments.py", + "colorama.py", + "tomli.py", + "exceptiongroup.py", ) for relative_path in cases: with self.subTest(relative_path=relative_path): @@ -549,6 +557,9 @@ def test_sourceless_pytest_bytecode_shadows_block_discovery(self) -> None: "pytest.pyc", "pytest/__init__.pyc", "pytest/__main__.pyc", + "pluggy.pyc", + "iniconfig/__init__.pyc", + "_pytest/__init__.pyc", ) for relative_path in cases: with self.subTest(relative_path=relative_path): @@ -567,6 +578,8 @@ def test_native_pytest_extension_shadows_block_discovery(self) -> None: cases = ( "pytest.cpython-312-x86_64-linux-gnu.so", "pytest/__main__.pyd", + "pluggy.cpython-312-x86_64-linux-gnu.so", + "_pytest/__init__.pyd", ) for relative_path in cases: with self.subTest(relative_path=relative_path): From 0b2195239baa6d1dc5467bbba783d1d19d9e9643 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:00:55 +0700 Subject: [PATCH 78/79] Block bundled py pytest shadows --- standards/lotus-family/ACCEPTANCE.md | 3 ++- standards/lotus-family/README.md | 3 ++- .../lotus-family/conformance/lotus_family_runtime_v2.py | 1 + .../conformance/test_exact_head_review_hardening.py | 6 ++++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/standards/lotus-family/ACCEPTANCE.md b/standards/lotus-family/ACCEPTANCE.md index d331f0f8..b2280999 100644 --- a/standards/lotus-family/ACCEPTANCE.md +++ b/standards/lotus-family/ACCEPTANCE.md @@ -4,7 +4,8 @@ - A regression test that is not discovered by configured CI produces `DRIFT`. - Explicit discovery commands target their checked `test_path`; every executed Python or Elixir test source must match its manifest-pinned SHA-256. -- Repository-local pytest or pytest-dependency modules and packages, +- Repository-local pytest or pytest-dependency modules and packages, including + the bundled `py` compatibility shim, sourceless bytecode, native extensions, and Python startup hooks that can preempt collection produce `DRIFT`. - Missing snapshots, refs, or exact commit identities produce `UNKNOWN`, never `PASS`. diff --git a/standards/lotus-family/README.md b/standards/lotus-family/README.md index a3a6ebe8..372051ce 100644 --- a/standards/lotus-family/README.md +++ b/standards/lotus-family/README.md @@ -43,7 +43,8 @@ test gate. The configured Python and Elixir contract-test sources must match pinned SHA-256 values, so retaining required phrases in a replacement no-op test cannot produce `PASS`. Pytest discovery also fails closed on repository-local pytest, -pytest-dependency, sourceless-bytecode, native-extension, or Python-startup +pytest-dependency (including the bundled `py` shim), sourceless-bytecode, +native-extension, or Python-startup shadows that could preempt the installed runner before contract collection. ## Causal spacetime testing model diff --git a/standards/lotus-family/conformance/lotus_family_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py index fbe308c5..ed54e7c2 100644 --- a/standards/lotus-family/conformance/lotus_family_runtime_v2.py +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -43,6 +43,7 @@ "colorama", "tomli", "exceptiongroup", + "py", ) diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index 4f2be52f..03c2c697 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -538,6 +538,8 @@ def test_repository_pytest_shadows_block_default_discovery(self) -> None: "colorama.py", "tomli.py", "exceptiongroup.py", + "py.py", + "py/__init__.py", ) for relative_path in cases: with self.subTest(relative_path=relative_path): @@ -559,6 +561,8 @@ def test_sourceless_pytest_bytecode_shadows_block_discovery(self) -> None: "pytest/__main__.pyc", "pluggy.pyc", "iniconfig/__init__.pyc", + "py.pyc", + "py/__init__.pyc", "_pytest/__init__.pyc", ) for relative_path in cases: @@ -579,6 +583,8 @@ def test_native_pytest_extension_shadows_block_discovery(self) -> None: "pytest.cpython-312-x86_64-linux-gnu.so", "pytest/__main__.pyd", "pluggy.cpython-312-x86_64-linux-gnu.so", + "py.cpython-312-x86_64-linux-gnu.so", + "py/__init__.pyd", "_pytest/__init__.pyd", ) for relative_path in cases: From 30d9f018e88a1a947a88406228cc7e91b9111218 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:31:35 +0700 Subject: [PATCH 79/79] Require automatic hosted workflow evidence --- .../lotus_family_workflow_policy_v4.py | 73 +++++++++++++++++++ .../conformance/test_causality_model.py | 1 + .../test_exact_head_review_hardening.py | 17 +++++ .../test_explicit_pytest_configuration.py | 1 + .../conformance/test_lotus_family_auditor.py | 8 +- .../conformance/test_policy_boundaries.py | 4 + .../conformance/test_policy_v3_boundaries.py | 2 + .../conformance/test_system_model.py | 4 + 8 files changed, 106 insertions(+), 4 deletions(-) diff --git a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py index f674a70f..45b513cb 100644 --- a/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py @@ -26,6 +26,7 @@ _PINNED_ACTION = re.compile( r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[0-9a-f]{40}$" ) +_AUTOMATIC_SOURCE_EVENTS = {"merge_group", "pull_request", "push"} _RUNNER_RESOLUTION_ENV_NAMES = { @@ -64,6 +65,74 @@ def _uses_direct_elixir(discovery: Mapping[str, Any]) -> bool: return False +def _workflow_tree( + text: str, +) -> tuple[ + list[str], + set[int], + dict[str, tuple[int, tuple[int, bool, int, str, str]]], +]: + """Return the direct workflow mapping without resolving YAML aliases.""" + lines = text.splitlines() + ranges = execution.legacy.scalar_ranges(lines) + scalar_body = { + row + for start, (end, _) in ranges.items() + for row in range(start + 1, end) + } + top = execution.base._properties(lines, 0, len(lines), -1, scalar_body) + return lines, scalar_body, top + + +def _has_automatic_source_trigger(text: str) -> bool: + """Require a literal source-change event, not manual-only dispatch.""" + lines, scalar_body, top = _workflow_tree(text) + trigger = top.get("on") + if trigger is None: + return False + + scalar = execution.legacy.inline_scalar(trigger[1][4]) + if scalar is not None: + value = scalar.strip() + if value.startswith("[") and value.endswith("]"): + decoded = { + execution.legacy.decode_key(item.strip()) + for item in value[1:-1].split(",") + if item.strip() + } + else: + decoded = {execution.legacy.decode_key(value)} + return bool(_AUTOMATIC_SOURCE_EVENTS & decoded) + + children = execution.base._mapping_children(lines, trigger, scalar_body) + return children is not None and bool( + _AUTOMATIC_SOURCE_EVENTS & set(children) + ) + + +def _has_job_container(text: str) -> bool: + """Reject unproven job containers that can replace tool executables.""" + lines, scalar_body, top = _workflow_tree(text) + jobs = top.get("jobs") + if jobs is None: + return False + children = execution.base._mapping_children(lines, jobs, scalar_body) + if children is None: + return False + for job_row, job_header in children.values(): + job_end = execution.legacy.block_end(lines, job_row, job_header[2]) + properties = execution.base._properties( + lines, + job_row + 1, + job_end, + job_header[2], + scalar_body, + ) + if "container" in properties: + return True + return False + + _TRUSTED_PREREQUISITE_COMMANDS = { ("mix", "local.hex", "--force"), ("mix", "local.rebar", "--force"), @@ -119,6 +188,10 @@ def _ci_discovery_one( discovery: Mapping[str, Any], workflow_text: str ) -> tuple[bool, list[str]]: """Find a gating test while failing closed on dynamic runner state.""" + if not _has_automatic_source_trigger(workflow_text): + return False, [] + if _has_job_container(workflow_text): + return False, [] if not policy_v2._workflow_execution_is_gating(workflow_text): return False, [] if policy_v2._uses_pytest(discovery) and policy_v2._has_unproven_pytest_env( diff --git a/standards/lotus-family/conformance/test_causality_model.py b/standards/lotus-family/conformance/test_causality_model.py index 65554f61..ca50a5e3 100644 --- a/standards/lotus-family/conformance/test_causality_model.py +++ b/standards/lotus-family/conformance/test_causality_model.py @@ -47,6 +47,7 @@ def _workflow(command: str) -> str: ) return ( "name: CI\n" + "on: push\n" "jobs:\n" " test:\n" " runs-on: ubuntu-latest\n" diff --git a/standards/lotus-family/conformance/test_exact_head_review_hardening.py b/standards/lotus-family/conformance/test_exact_head_review_hardening.py index 03c2c697..a5b760aa 100644 --- a/standards/lotus-family/conformance/test_exact_head_review_hardening.py +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -33,6 +33,7 @@ def _workflow(*, shell: str | None = None, job_continue: str | None = None) -> s ) return ( "name: CI\n" + "on: push\n" "jobs:\n" " test:\n" " runs-on: ubuntu-latest\n" @@ -163,6 +164,21 @@ def _materialize_pythia(snapshot_root: Path, manifest: dict) -> Path: class WorkflowReviewHardeningTest(unittest.TestCase): """Protect workflow failure propagation and gating semantics.""" + def test_manual_only_workflow_is_not_ci_evidence(self) -> None: + manual_only = _workflow().replace( + "on: push\n", "on: workflow_dispatch\n", 1 + ) + self.assertEqual(ci_discovery(DISCOVERY, manual_only), (False, [])) + + def test_job_container_is_not_ci_evidence(self) -> None: + containerized = _workflow().replace( + " runs-on: ubuntu-latest\n", + " runs-on: ubuntu-latest\n" + " container: attacker.example/fake-python:latest\n", + 1, + ) + self.assertEqual(ci_discovery(DISCOVERY, containerized), (False, [])) + def test_custom_shell_without_fail_fast_is_rejected(self) -> None: self.assertEqual( ci_discovery(DISCOVERY, _workflow(shell="bash {0}")), @@ -226,6 +242,7 @@ def test_direct_elixir_vm_flag_environment_is_rejected(self) -> None: ) workflow = ( "name: CI\n" + "on: push\n" "env:\n" " ERL_AFLAGS: -eval halt().\n" "jobs:\n" diff --git a/standards/lotus-family/conformance/test_explicit_pytest_configuration.py b/standards/lotus-family/conformance/test_explicit_pytest_configuration.py index de1b69f4..0b13acbb 100644 --- a/standards/lotus-family/conformance/test_explicit_pytest_configuration.py +++ b/standards/lotus-family/conformance/test_explicit_pytest_configuration.py @@ -54,6 +54,7 @@ def _materialize_ls(snapshot_root: Path, manifest: dict) -> Path: config["ci_discovery"]["workflow_paths"][0], ( "name: CI\n" + "on: push\n" "jobs:\n" " test:\n" " runs-on: ubuntu-latest\n" diff --git a/standards/lotus-family/conformance/test_lotus_family_auditor.py b/standards/lotus-family/conformance/test_lotus_family_auditor.py index 37206d50..f8d4b9e2 100644 --- a/standards/lotus-family/conformance/test_lotus_family_auditor.py +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -8,7 +8,7 @@ def workflow(command, env="", shell=""): env_block = "env:\n" + "\n".join(f" {x}" for x in env.splitlines()) + "\n" if env else "" shell_line = f" shell: {shell}\n" if shell else "" body = "\n".join(f" {x}" if x else "" for x in command.splitlines()) - return ("name: CI\n" + env_block + "jobs:\n test:\n runs-on: ubuntu-latest\n" + return ("name: CI\non: push\n" + env_block + "jobs:\n test:\n runs-on: ubuntu-latest\n" " steps:\n - name: Run tests\n" + shell_line + " run: |\n" + body + "\n") @@ -50,19 +50,19 @@ def bare(self): def workflow_env_text(self): - self.assert_ci_drift("cml", "name: CI\nenv:\n NOTE: |\n python -m pytest\n" + self.assert_ci_drift("cml", "name: CI\non: push\nenv:\n NOTE: |\n python -m pytest\n" "jobs:\n test:\n runs-on: ubuntu-latest\n steps:\n" " - uses: actions/checkout@v4\n", raw=True) def step_env_text(self): - self.assert_ci_drift("cml", "name: CI\njobs:\n test:\n runs-on: ubuntu-latest\n" + self.assert_ci_drift("cml", "name: CI\non: push\njobs:\n test:\n runs-on: ubuntu-latest\n" " steps:\n - name: Metadata only\n env:\n" " NOTE: python -m pytest\n uses: actions/checkout@v4\n", raw=True) def fake_steps_in_metadata(self): - self.assert_ci_drift("cml", "name: CI\nmetadata:\n fake-job:\n steps:\n" + self.assert_ci_drift("cml", "name: CI\non: push\nmetadata:\n fake-job:\n steps:\n" " - run: python -m pytest\njobs: {}\n", raw=True) diff --git a/standards/lotus-family/conformance/test_policy_boundaries.py b/standards/lotus-family/conformance/test_policy_boundaries.py index fcacf884..0bb5199e 100644 --- a/standards/lotus-family/conformance/test_policy_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_boundaries.py @@ -17,6 +17,7 @@ def workflow(command: str, *, env: str = "") -> str: ) return ( "name: CI\n" + "on: push\n" f"{env_block}" "jobs:\n" " test:\n" @@ -32,6 +33,7 @@ def workflow_steps(steps: str) -> str: """Build one workflow from already indented step-list entries.""" return ( "name: CI\n" + "on: push\n" "jobs:\n" " test:\n" " runs-on: ubuntu-latest\n" @@ -80,6 +82,7 @@ def test_workflow_documents_are_evaluated_independently(self) -> None: def test_anchored_pytest_env_mapping_blocks_discovery(self) -> None: text = ( "name: CI\n" + "on: push\n" "env: &pytest_env\n" " PYTEST_ADDOPTS: " "--ignore=tests/test_lotus_docs_contract.py\n" @@ -95,6 +98,7 @@ def test_anchored_pytest_env_mapping_blocks_discovery(self) -> None: def test_aliased_env_mapping_fails_closed(self) -> None: text = ( "name: CI\n" + "on: push\n" "pytest-env: &pytest_env\n" " SAFE_VALUE: true\n" "env: *pytest_env\n" diff --git a/standards/lotus-family/conformance/test_policy_v3_boundaries.py b/standards/lotus-family/conformance/test_policy_v3_boundaries.py index 1069e777..7105a6cd 100644 --- a/standards/lotus-family/conformance/test_policy_v3_boundaries.py +++ b/standards/lotus-family/conformance/test_policy_v3_boundaries.py @@ -17,6 +17,7 @@ def workflow_steps(steps: str) -> str: """Build one runnable workflow from indented step-list entries.""" return ( "name: CI\n" + "on: push\n" "jobs:\n" " test:\n" " runs-on: ubuntu-latest\n" @@ -129,6 +130,7 @@ def test_yaml_command_resolution_environment_is_rejected(self) -> None: with self.subTest(name=name): text = ( "name: CI\n" + "on: push\n" f"{workflow_env}" "jobs:\n" " test:\n" diff --git a/standards/lotus-family/conformance/test_system_model.py b/standards/lotus-family/conformance/test_system_model.py index 9a43d118..2b5cad3b 100644 --- a/standards/lotus-family/conformance/test_system_model.py +++ b/standards/lotus-family/conformance/test_system_model.py @@ -53,6 +53,7 @@ def workflow( ) return ( "name: CI\n" + "on: push\n" f"{workflow_extra}" "jobs:\n" f"{prefix_jobs}" @@ -70,6 +71,7 @@ def workflow( SCENARIOS = { "ci-nonrun-001": ( "name: CI\n" + "on: push\n" "env:\n" " NOTE: python -m pytest\n" "jobs: {}\n" @@ -84,6 +86,7 @@ def workflow( ), "ci-no-runner-001": ( "name: CI\n" + "on: push\n" "jobs:\n" " test:\n" " steps:\n" @@ -111,6 +114,7 @@ def workflow( ), "ci-fake-steps-001": ( "name: CI\n" + "on: push\n" "metadata:\n" " fake:\n" " steps:\n"