diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b872721f..9a1c2507 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,18 +10,21 @@ 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: 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@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/.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 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__/ diff --git a/standards/lotus-family/ACCEPTANCE.md b/standards/lotus-family/ACCEPTANCE.md new file mode 100644 index 00000000..b2280999 --- /dev/null +++ b/standards/lotus-family/ACCEPTANCE.md @@ -0,0 +1,13 @@ +# Acceptance criteria + +- 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`; every executed + Python or Elixir test source must match its manifest-pinned SHA-256. +- 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`. +- 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/DECISION-CODES.md b/standards/lotus-family/DECISION-CODES.md new file mode 100644 index 00000000..50319cec --- /dev/null +++ b/standards/lotus-family/DECISION-CODES.md @@ -0,0 +1,15 @@ +# Lotus Family audit result codes + +| Outcome | Reason code | Meaning | +|---|---|---| +| `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 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/IMPLEMENTATION.md b/standards/lotus-family/IMPLEMENTATION.md new file mode 100644 index 00000000..75385bb7 --- /dev/null +++ b/standards/lotus-family/IMPLEMENTATION.md @@ -0,0 +1,9 @@ +# 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. + +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/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. diff --git a/standards/lotus-family/README.md b/standards/lotus-family/README.md new file mode 100644 index 00000000..372051ce --- /dev/null +++ b/standards/lotus-family/README.md @@ -0,0 +1,106 @@ +# 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 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, 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. + +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. + +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. + +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 (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 + +The test model has two compatible layers: + +- `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. + +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 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 + +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 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/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 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 new file mode 100644 index 00000000..c32d14de --- /dev/null +++ b/standards/lotus-family/ROADMAP.md @@ -0,0 +1,20 @@ +# Lotus Family auditor roadmap + +## v0.1 + +- manifest for Pythia, CML, and LS; +- 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 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 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; +- generate selected regression fixtures from causal routes while keeping human review. 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 diff --git a/standards/lotus-family/causality/SYSTEM-MODEL.md b/standards/lotus-family/causality/SYSTEM-MODEL.md new file mode 100644 index 00000000..6d6fc9d0 --- /dev/null +++ b/standards/lotus-family/causality/SYSTEM-MODEL.md @@ -0,0 +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. + +## Five dimensions + +- **Causal** — why a condition changes risk or outcome. +- **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` — 49 nodes, 64 unique directed + relationships, centers, and four canonical trajectories; +- `system-routes-v0.1.json` — 36 connected routes, including runtime + regressions and governance-only merge trajectories. + +## Centers without centralized authority + +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. + +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 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 + +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; +3. include an executable route when runtime behavior is involved; +4. preserve the boundary `PASS != APPROVED != MERGED`. 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/lotus-family-system-v0.1.json b/standards/lotus-family/causality/lotus-family-system-v0.1.json new file mode 100644 index 00000000..955a2d9a --- /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","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 new file mode 100644 index 00000000..f96cf2fa --- /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",["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 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/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. diff --git a/standards/lotus-family/conformance/__init__.py b/standards/lotus-family/conformance/__init__.py new file mode 100644 index 00000000..e69de29b 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.py b/standards/lotus-family/conformance/lotus_family_auditor.py new file mode 100644 index 00000000..b7400093 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_auditor.py @@ -0,0 +1,7 @@ +from lotus_family_runtime_v3 import * # noqa: F403 +from lotus_family_schema import DRIFT, PASS, UNKNOWN, load_manifest +from lotus_family_runtime_v3 import main + + +if __name__ == "__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 new file mode 100644 index 00000000..1a1760cf --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_auditor_core.py @@ -0,0 +1,2 @@ +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_auditor_legacy_tests.py b/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py new file mode 100644 index 00000000..e5d4ce14 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_auditor_legacy_tests.py @@ -0,0 +1,330 @@ +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_test_sources import pinned_test_source + +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", + ) + 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"] + _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/lotus_family_runtime.py b/standards/lotus-family/conformance/lotus_family_runtime.py new file mode 100644 index 00000000..c54b282c --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_runtime.py @@ -0,0 +1,283 @@ +"""Lotus Family snapshot audit runtime.""" + +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]: + """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 + ) + + 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 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=[], + ) + + 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=( + "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, + } + ) + 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] + 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": check_detail, + } + ) + if check_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, ci_detail = DRIFT, "; ".join(errors) + elif discovered: + ci_outcome = PASS + ci_detail = ( + "contract regression test is discovered by executable CI" + ) + else: + 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": ci_detail, + } + ) + + if drift: + audit_outcome, reason = DRIFT, "LOTUS_CONTRACT_DRIFT" + detail = "non-conforming checks: " + ", ".join( + sorted(set(drift)) + ) + else: + 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=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: + """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) + 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_runtime_v2.py b/standards/lotus-family/conformance/lotus_family_runtime_v2.py new file mode 100644 index 00000000..ed54e7c2 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_runtime_v2.py @@ -0,0 +1,696 @@ +"""Runtime hardening overlay for Lotus Family conformance v0.1.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +from collections.abc import Mapping +from pathlib import Path, PurePosixPath +from typing import Any + +import lotus_family_runtime as previous +import tomllib +from lotus_family_schema import ( + DRIFT, + PASS, + UNKNOWN, + load_manifest, + manifest_invalid, + read_file, + repository_config, +) + +_PYTEST_CONFIG_PATHS = ( + "pytest.toml", + ".pytest.toml", + "pytest.ini", + ".pytest.ini", + "pyproject.toml", + "tox.ini", + "setup.cfg", +) + + +_PYTEST_IMPORT_MODULES = ( + "pytest", + "_pytest", + "pluggy", + "iniconfig", + "packaging", + "pygments", + "colorama", + "tomli", + "exceptiongroup", + "py", +) + + +_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", +) + + +_PYTHON_STARTUP_SHADOW_PATHS = ( + "sitecustomize.py", + "sitecustomize.pyc", + "usercustomize.py", + "usercustomize.pyc", +) + + +_DEDICATED_PYTEST_CONFIGS = { + "pytest.toml", + ".pytest.toml", + "pytest.ini", + ".pytest.ini", +} + + +_MIX_CONFIG_PATHS = ("mix.exs", "test/test_helper.exs") + + +_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_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__\)' +) + + +_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"} + + +_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_IMPORT_MODULES, "sitecustomize", "usercustomize"), + ): + discovered.append(candidate.name) + + 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: + package_entries = [] + for candidate in package_entries: + if _looks_like_import_shadow( + candidate.name, + entry_stems, + ): + discovered.append(f"{package_name}/{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_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, + ) + + +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 _is_conftest(path: str) -> bool: + """Return true for a repository-relative pytest conftest path.""" + 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 _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_import_shadow_path(path): + return True + if _is_conftest(path): + return True + if path in _DEDICATED_PYTEST_CONFIGS: + return _has_meaningful_lines(text) + if path == "pyproject.toml": + 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": + return bool( + re.search( + r"(?mi)^\s*\[\s*(?:tool:pytest|pytest)\s*\]\s*$", + text, + ) + ) + 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 pytest configs and fail closed on active, unreadable, or hook files.""" + observed: list[str] = [] + blockers: list[str] = [] + 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): + 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 _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 + if _activates_pytest_configuration(relative_path, text): + blockers.append(relative_path) + 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 _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 + + 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 + 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] + 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) + 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 + + required_files = _mix_wrapper( + text, + definition.start(), + definition_end, + ) + if required_files is None: + 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 + 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 + + +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: + """Detect Mix and ExUnit settings that can hide contract tests.""" + if path == "mix.exs": + 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": + 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 + + +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] = [] + 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(): + 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 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 + # 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 + + +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 not isinstance(discovery, Mapping): + return audit + + repository_root = ( + snapshot_root.resolve() / str(config["snapshot_dir"]) + ).resolve() + files = list(audit.get("files", [])) + 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 all_blockers: + audit["outcome"] = DRIFT + audit["reason_code"] = "LOTUS_CONTRACT_DRIFT" + 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 + + +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"] 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"] 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..c70aa30a --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_schema.py @@ -0,0 +1,255 @@ +from __future__ import annotations + +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}$" +) + + +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 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") + 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() + 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): + 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) + 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") + 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}]") + 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") + 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" + ) + for pi, pattern in enumerate(patterns): + field = f"{prefix}.ci_discovery.contains_any[{pi}]" + _, targets = command_test_paths(pattern, field) + if targets != {test_path}: + raise ValueError( + f"{field} must target only ci_discovery.test_path" + ) + 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}") + 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]: + 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() + 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__}" + + +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_system_model.py b/standards/lotus-family/conformance/lotus_family_system_model.py new file mode 100644 index 00000000..71d52a0c --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_system_model.py @@ -0,0 +1,208 @@ +"""Validation and derived views for the Lotus system graph.""" + +from __future__ import annotations + +import json +from collections import Counter +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]]: + """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") + 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 raw_rows: + 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]: + """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", + } + 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[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']}") + 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: + """Depth-first cycle check.""" + 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]]: + """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() + 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]: + """Compute review-priority centrality without assigning authority.""" + view = validate_graph(graph) + degree: Counter[str] = Counter() + for edge in view["edges"]: + degree[edge["source"]] += 1 + degree[edge["target"]] += 1 + 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"]) + ) + 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: + """Render a human-readable ledger derived from executable routes.""" + lines = [ + "# Lotus system route ledger", + "", + "| Route | Repository | Scenario | Trajectory | Expected |", + "|---|---|---|---|---|", + ] + for route in routes: + lines.append( + 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_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/lotus_family_workflow.py b/standards/lotus-family/conformance/lotus_family_workflow.py new file mode 100644 index 00000000..83119030 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow.py @@ -0,0 +1,3 @@ +"""Public fail-closed GitHub Actions discovery API.""" + +from lotus_family_workflow_policy_v4 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..a742d69a --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_hardened.py @@ -0,0 +1,418 @@ +"""Fail-closed GitHub Actions discovery for Lotus regression tests.""" + +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"} +_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 + 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: + """Accept only explicit POSIX shells whose script placeholder is executable.""" + 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(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 + 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 _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) + } + 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] = [] + + 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 + job_if = job_properties.get("if") + if job_if is None or _condition(job_if[1][4]) != "always()": + continue + 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 + 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=";&|(){}") + 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: + """Tokenize one direct command under the fail-closed shell policy.""" + 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: + """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 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 argument in parts[2:]: + if argument in legacy.SAFE_MIX_FLAGS or argument.startswith( + legacy.SAFE_MIX_PREFIXES + ): + continue + if argument.startswith("-") or ":" in argument: + return False + positive.append(legacy.normalize(argument)) + 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]]: + """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) + ] + 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..8dc9af75 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_legacy.py @@ -0,0 +1,440 @@ +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"} +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: + 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 YAML_ALIAS_ONLY.fullmatch(inline): + names.add(UNRESOLVED_ENV_MAPPING) + continue + if YAML_ANCHOR_ONLY.fullmatch(inline): + inline = "" + if inline: + 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 + 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/lotus_family_workflow_policy.py b/standards/lotus-family/conformance/lotus_family_workflow_policy.py new file mode 100644 index 00000000..9abc432c --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy.py @@ -0,0 +1,477 @@ +"""Final fail-closed execution policy for Lotus GitHub Actions discovery.""" + +from __future__ import annotations + +import re +import shlex +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"} +_TERMINATORS = {"exit", "exec", "return", "break", "continue"} +_DIRECTORY_MUTATORS = {"cd", "pushd", "popd", "source", "."} +_CONTROL = legacy.CONTROL_TOKENS | {"(", ")", "{", "}"} + + +def _supported_shell(value: str) -> bool: + """Accept only exact 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 or parts[0] not in _POSIX_SHELLS: + return False + if len(parts) == 1: + return True + allowed = { + ("bash", "-e", "{0}"), + ("bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "{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: + """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 _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, + 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 = { + 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]) + ) + + 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: + 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): + 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) + + 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]) + 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 = _boolean_entry(step.get("if"), default=True) + if step_if is False: + continue + if step_if is not True: + blocked = True + break + + uses = step.get("uses") + run = step.get("run") + if uses is not None: + 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( + step, job_defaults, workflow_defaults, "shell" + ) + 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( + step, job_defaults, workflow_defaults, "working-directory" + ) + if not base._repo_root_working_directory(working_directory): + 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]] + 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: + """Reject commands that can end execution or change later command meaning.""" + if not parts: + return False + command = parts[0] + if command == "eval": + return True + 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 _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 _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 "invalid", None + try: + lexer = shlex.shlex( + stripped, posix=True, punctuation_chars=";&|(){}" + ) + lexer.whitespace_split = True + lexer.commenters = "" + tokens = list(lexer) + except ValueError: + 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) + ): + 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 "invalid", None + if parsed and ( + legacy.ENV_ASSIGNMENT.fullmatch(parsed[0]) + or _unsafe_state_change(parsed) + ): + return "invalid", None + if parsed and _safe_prelude(parsed): + saw_prelude = True + index += 1 + continue + 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.""" + 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", []) + ) + if uses_pytest and ( + 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() + ), + ) + for script in scripts + ) + ): + return False, [] + + matches: list[str] = [] + 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 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 + + +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__ = [ + "ci_discovery", + "github_run_scripts", + "shell_commands", + "pytest_command", + "mix_command", +] 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..1f9f45c8 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v2.py @@ -0,0 +1,253 @@ +"""Final review hardening overlay for Lotus workflow discovery.""" + +from __future__ import annotations + +import re +import shlex +from typing import Any, Mapping + +import lotus_family_workflow_policy as previous + + +_NON_FAIL_FAST_CUSTOM_SHELLS = { + ("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") + + +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 _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 ( + _uses_non_fail_fast_custom_shell(text) + or _has_non_gating_job(text) + ) + + +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): + return [] + return previous.github_run_scripts(text) + + +def _ci_discovery_one( + discovery: Mapping[str, Any], workflow_text: str +) -> tuple[bool, list[str]]: + """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, [] + + 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( + 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", +] 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..b256fec5 --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v3.py @@ -0,0 +1,139 @@ +"""Exact-head review fixes for Lotus workflow discovery reachability.""" + +from __future__ import annotations + +import re +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"} +_COMMAND_RESOLUTION_MUTATION = re.compile( + r"(?:\bGITHUB_PATH\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, +) + + +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 _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]]: + """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: + if _mutates_command_resolution(script): + break + 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, command-resolution mutation, 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", +] 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..45b513cb --- /dev/null +++ b/standards/lotus-family/conformance/lotus_family_workflow_policy_v4.py @@ -0,0 +1,307 @@ +"""Exact-head review fixes for dynamic runner-state and test arguments.""" + +from __future__ import annotations + +import re +import shlex +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 +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"(?\()" +) +_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 = { + "BASH_ENV", + "ENV", + "ERL_LIBS", + "ELIXIR_ERL_OPTIONS", + "ERL_AFLAGS", + "ERL_FLAGS", + "ERL_ZFLAGS", + "MIX_ARCHIVES", + "MIX_HOME", + "MIX_PATH", + "PATH", + "PYTHONHOME", + "PYTHONPATH", +} + + +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 + + +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"), + ("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( + 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 _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]: + """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 _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( + workflow_text + ): + return False, [] + + 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 ( + 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 ( + any(name.startswith("PYTEST_") for name in yaml_env_names) + 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 + 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 + + +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", + "mix_command", + "pytest_command", + "shell_commands", +] 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..ca50a5e3 --- /dev/null +++ b/standards/lotus-family/conformance/test_causality_model.py @@ -0,0 +1,313 @@ +"""Executable checks for the original Lotus causal route model.""" + +from __future__ import annotations + +import copy +import json +import tempfile +import unittest +from pathlib import Path + +from lotus_family_auditor import ( + audit_repository, + load_manifest, +) +from lotus_family_test_sources import pinned_test_source + +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 +AUTHORITY_GRANTS = ( + "grants_ownership", + "grants_approval", + "grants_execution", + "grants_delivery", + "grants_merge", +) + + +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" + "on: push\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", + ) + 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"] + _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, + ) -> 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) + for edge in self.graph["edges"]: + self.assertIn(edge["source"], known) + self.assertIn(edge["target"], known) + + 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"]) + visiting: set[str] = set() + visited: set[str] = set() + + def visit(node: str) -> None: + if node in visiting: + self.fail( + "causality graph contains a cycle " + f"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, + ) -> 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) + 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, + ) -> 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"] + } + self.assertEqual(required_nodes - covered, set()) + + 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 + ) + + 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 + ) + 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.assertEqual( + result["authority"]["mode"], + "audit_only", + ) + for grant in AUTHORITY_GRANTS: + self.assertFalse( + result["authority"][grant], + f"{route['id']} granted {grant}", + ) + + +if __name__ == "__main__": + unittest.main() 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..a5b760aa --- /dev/null +++ b/standards/lotus-family/conformance/test_exact_head_review_hardening.py @@ -0,0 +1,901 @@ +"""Regression checks for the final exact-head review findings.""" + +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 +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" + "on: push\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 _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"] == repository_id + ) + 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", + ) + 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, + 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" + ) + 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", + ) + 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"] + if discovery["strategy"] == "mix_default_discovery": + command = discovery["command"] + else: + command = discovery["contains_any"][0] + _write( + repository_root, + discovery["workflow_paths"][0], + _workflow().replace("python -m pytest", command), + ) + return repository_root + + +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}")), + (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, []), + ) + + 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, + [ + ( + "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" + "on: push\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, []), + ) + + +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 executed test source" + else: + test_check["sha256"] = None + error = "sha256 must be a lowercase SHA-256" + + 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) + 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.""" + + 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 _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) + 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._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_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) + 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_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_repository_pytest_shadows_block_default_discovery(self) -> None: + cases = ( + "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", + "py.py", + "py/__init__.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_sourceless_pytest_bytecode_shadows_block_discovery(self) -> None: + cases = ( + "pytest.pyc", + "pytest/__init__.pyc", + "pytest/__main__.pyc", + "pluggy.pyc", + "iniconfig/__init__.pyc", + "py.pyc", + "py/__init__.pyc", + "_pytest/__init__.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", + "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: + 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) + 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"]}, + ) + + +class MixConfigurationReviewHardeningTest(unittest.TestCase): + """Bind Mix default discovery to hashed collection configuration.""" + + def setUp(self) -> None: + 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( + 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_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_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_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) + 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"} + ) + + 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 + ) + _write( + repository_root, + "mix.exs", + ( + '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"], DRIFT) + check = next( + row + for row in result["checks"] + if row["check_id"] == "mix_configuration" + ) + 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__": + unittest.main() 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..0b13acbb --- /dev/null +++ b/standards/lotus-family/conformance/test_explicit_pytest_configuration.py @@ -0,0 +1,145 @@ +"""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 +from lotus_family_test_sources import pinned_test_source + + +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", + ) + 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], + ( + "name: CI\n" + "on: push\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 + + +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.""" + + 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_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("pytest.ini", config_check["blocked_paths"]) + self.assertIn( + "pytest.ini", + {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() 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..f8d4b9e2 --- /dev/null +++ b/standards/lotus-family/conformance/test_lotus_family_auditor.py @@ -0,0 +1,128 @@ +import tempfile +import unittest +from pathlib import Path +import lotus_family_auditor_legacy_tests as old + + +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\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") + + +def fixture(discovery): + 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" + return workflow(command) + + +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, shell), 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"], []) + + +old._discovery_fixture = fixture +old.LotusFamilyAuditorTest.assert_ci_drift = assert_drift + + +def bare(self): + self.assert_ci_drift("cml", "python -m pytest\n", raw=True) + + +def workflow_env_text(self): + 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\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\non: push\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") + + +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 yaml_addopts(self): + self.assert_ci_drift("cml", "python -m pytest\n", + env="PYTEST_ADDOPTS: --ignore=tests/test_lotus_docs_contract.py") + + +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, + "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, + "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) + +LotusFamilyAuditorTest = old.LotusFamilyAuditorTest + +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 new file mode 100644 index 00000000..0bb5199e --- /dev/null +++ b/standards/lotus-family/conformance/test_policy_boundaries.py @@ -0,0 +1,297 @@ +"""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: + """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() + ) + return ( + "name: CI\n" + "on: push\n" + f"{env_block}" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - name: Run tests\n" + " run: |\n" + f"{body}\n" + ) + + +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" + " steps:\n" + f"{steps}" + ) + + +DISCOVERY = { + "strategy": "pytest_default_discovery", + "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", +} +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): + """Exercise public fail-closed workflow discovery boundaries.""" + + 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_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" + "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" + "on: push\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( + 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"]), + ) + + 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_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_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" + " 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, []), + ) + + 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 = ( + ( + 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.""" + + 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() 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..7105a6cd --- /dev/null +++ b/standards/lotus-family/conformance/test_policy_v3_boundaries.py @@ -0,0 +1,151 @@ +"""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" + "on: push\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + f"{steps}" + ) + + +class WorkflowPolicyV3BoundaryTest(unittest.TestCase): + """Keep unknown policy, blockers, and runner resolution 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, [])) + + 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_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_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\"", + "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, [])) + + 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", + ), + ( + "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): + text = ( + "name: CI\n" + "on: push\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() 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..2b5cad3b --- /dev/null +++ b/standards/lotus-family/conformance/test_system_model.py @@ -0,0 +1,478 @@ +"""Executable validation for the Lotus causal spacetime system model.""" + +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, +) +from lotus_family_test_sources import pinned_test_source + +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 +AUTHORITY_GRANTS = ( + "grants_ownership", + "grants_approval", + "grants_execution", + "grants_delivery", + "grants_merge", +) + + +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" + "on: push\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\n" + "on: push\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" + "on: push\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" + "on: push\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-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", + prefix_jobs=( + " gate:\n" + " if: ${{ false }}\n" + " runs-on: ubuntu-latest\n" + " steps:\n" + " - 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=( + " 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] + if pattern.endswith(".py"): + return f"python -m pytest {pattern}" + if pattern.endswith(".exs"): + return f"mix test {pattern}" + return pattern + + +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"] + ) + for name, values in terms.items(): + path = repository / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + "\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( + pinned_test_source( + config["id"], + check["path"], + check["sha256"], + ), + encoding="utf-8", + ) + discovery = config["ci_discovery"] + 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 repository + + +class SystemModelTest(unittest.TestCase): + """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.manifest = load_manifest(MANIFEST) + + def test_graph_has_five_dimensions_centers_and_trajectories( + self, + ) -> None: + view = validate_graph(self.graph) + self.assertEqual(len(view["nodes"]), 49) + self.assertEqual(len(view["edges"]), 64) + self.assertEqual(len(self.routes), 36) + 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, + ) -> None: + report = centrality_report(self.graph, self.routes) + self.assertIn( + "never ownership or authority", report["meaning"] + ) + self.assertEqual(len(report["nodes"]), 49) + + 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, + ) -> 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"] + ) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + if route["scenario"] != "missing_snapshot": + repository = materialize( + root, + config, + SCENARIOS.get(route["scenario"]), + ) + if route["scenario"] == "missing_term": + check = config["file_checks"][0] + 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=repository_ref, + commit_sha=commit_sha, + ) + self.assertEqual( + ( + result["outcome"], + result["reason_code"], + ), + ( + route["outcome"], + route["reason"], + ), + ) + for grant in AUTHORITY_GRANTS: + self.assertFalse( + result["authority"][grant], + f"{route['id']} granted {grant}", + ) + + 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__": + unittest.main() 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..f3f5e1b5 --- /dev/null +++ b/standards/lotus-family/manifest/lotus-family-v0.1.json @@ -0,0 +1,169 @@ +{ + "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", + "sha256": "3bd2304e5aef290666adc6c58bdd731ca1194f77ab64ebbc19c04c7a263ceaaf", + "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" + ], + "trusted_prerequisite_actions": [ + "actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803", + "erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124", + "dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c", + "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 --" + ] + } + }, + { + "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", + "sha256": "69de6a265928685d814d7bd1d77719a090a45ed4e9eecee9677c2d2e8259e5df", + "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" + ], + "strategy": "pytest_default_discovery", + "command": "python -m pytest", + "test_path": "tests/test_lotus_docs_contract.py" + } + }, + { + "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", + "sha256": "86662af527b02e0d950feaba66819ddfdd3dcccbb8b10b125bf8085e259d4589", + "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" + ], + "strategy": "contains_any", + "test_path": "tests/test_lotus_docs_contract.py", + "contains_any": [ + "tests/test_lotus_docs_contract.py" + ] + } + } + ] +}