diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml new file mode 100644 index 0000000..fb8073a --- /dev/null +++ b/.github/workflows/conformance.yml @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: MPL-2.0 +# conformance.yml — parser conformance gate for the Phronesis grammar. +# +# Runs the real parser (Phronesis.parse/1 = Lexer.tokenize |> Parser.parse) over +# the conformance corpus: +# * conformance/valid/*.phr MUST parse (exit 0) +# * conformance/invalid/*.phr MUST fail (exit non-zero) +# The corpus is the executable contract for spec/grammar.ebnf (v0.2.0). Any drift +# between the parser and the documented grammar turns this gate red. +name: Conformance + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +# Estate guardrail: cancel superseded runs (read-only check, safe to cancel). +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + conformance: + name: Grammar conformance (parser vs corpus) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # Match the project toolchain (.tool-versions: elixir 1.16.0 / erlang 26.2.1). + # Pinned to the estate-vetted setup-beam SHA already used by hypatia-scan.yml. + - name: Setup Elixir + uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.18.2 + with: + elixir-version: '1.16' + otp-version: '26' + + - name: Fetch dependencies + run: mix deps.get + + # Compile without --warnings-as-errors: the gate measures conformance, not + # lint. A genuine compile break still fails here (mix compile exits non-zero). + - name: Compile + run: mix compile + + - name: Run conformance corpus + run: ./conformance/run_conformance.sh diff --git a/conformance/run_conformance.sh b/conformance/run_conformance.sh index 452e88f..6e740de 100755 --- a/conformance/run_conformance.sh +++ b/conformance/run_conformance.sh @@ -20,7 +20,7 @@ if [[ -n "${1:-}" ]]; then # shellcheck disable=SC2206 # deliberate word-splitting of caller's string PARSER_CMD=(${1}) else - PARSER_CMD=(mix run -e 'Phronesis.CLI.parse(System.argv())' --) + PARSER_CMD=(mix run --no-start -e 'System.halt(case Phronesis.parse(File.read!(Enum.at(System.argv(), 0))) do {:ok, _} -> 0; _ -> 1 end)' --) fi PASS=0 diff --git a/conformance/valid/v02_if_then.phr b/conformance/valid/v02_if_then.phr index 9d8e647..264f418 100644 --- a/conformance/valid/v02_if_then.phr +++ b/conformance/valid/v02_if_then.phr @@ -1,11 +1,9 @@ # SPDX-License-Identifier: MPL-2.0 -# Conformance: IF/THEN/ELSE conditional in policy +# Conformance: IF/THEN/ELSE conditional action (grammar §5 conditional_action) POLICY conditional_check: - IF status == :valid THEN - ACCEPT "Status is valid" - ELSE - REJECT "Status is invalid" -PRIORITY 50 -EXPIRES "2030-12-31" -CREATED_BY "test_suite" + value > 0 + THEN IF value > 100 THEN ACCEPT("High value") ELSE REJECT("Low value") + PRIORITY: 50 + EXPIRES: never + CREATED_BY: test_suite diff --git a/conformance/valid/v03_and_or.phr b/conformance/valid/v03_and_or.phr index 87ef28f..ea51e4d 100644 --- a/conformance/valid/v03_and_or.phr +++ b/conformance/valid/v03_and_or.phr @@ -1,11 +1,11 @@ # SPDX-License-Identifier: MPL-2.0 -# Conformance: compound conditions with AND/OR +# Conformance: compound conditions with AND/OR (grammar §6 logical_expr) +# AND/OR are equal-precedence, left-associative, so this parses as +# ((score > 50 AND status == "active") OR override == true). POLICY compound_logic: - IF (score > 50 AND status == :active) OR override == true THEN - ACCEPT "Conditions met" - ELSE - REJECT "Conditions not met" -PRIORITY 75 -EXPIRES "2030-12-31" -CREATED_BY "test_suite" + score > 50 AND status == "active" OR override == true + THEN ACCEPT("Conditions met") + PRIORITY: 75 + EXPIRES: never + CREATED_BY: test_suite diff --git a/conformance/valid/v05_import.phr b/conformance/valid/v05_import.phr index cca9cae..efca951 100644 --- a/conformance/valid/v05_import.phr +++ b/conformance/valid/v05_import.phr @@ -1,15 +1,12 @@ # SPDX-License-Identifier: MPL-2.0 -# Conformance: IMPORT clause +# Conformance: IMPORT clause + module-qualified call (grammar §3, §8) IMPORT Std.BGP IMPORT Std.RPKI POLICY import_test: - rpki_result = Std.RPKI.validate(route) - IF rpki_result == :valid THEN - ACCEPT "RPKI validation passed" - ELSE - REJECT "RPKI validation failed" -PRIORITY 100 -EXPIRES "2030-12-31" -CREATED_BY "test_suite" + Std.RPKI.validate(route) == "valid" + THEN ACCEPT("RPKI validation passed") + PRIORITY: 100 + EXPIRES: never + CREATED_BY: test_suite diff --git a/conformance/valid/v07_nested.phr b/conformance/valid/v07_nested.phr index b20bead..fc5ea57 100644 --- a/conformance/valid/v07_nested.phr +++ b/conformance/valid/v07_nested.phr @@ -1,14 +1,9 @@ # SPDX-License-Identifier: MPL-2.0 -# Conformance: nested conditions +# Conformance: nested conditional actions (grammar §5 conditional_action) POLICY nested_check: - IF level > 1 THEN - IF score > 80 THEN - ACCEPT "High score at high level" - ELSE - REJECT "Low score at high level" - ELSE - ACCEPT "Any score at base level" -PRIORITY 150 -EXPIRES "2030-12-31" -CREATED_BY "test_suite" + level > 1 + THEN IF score > 80 THEN ACCEPT("High score at high level") ELSE REJECT("Low score at high level") + PRIORITY: 150 + EXPIRES: never + CREATED_BY: test_suite