Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2f6b619
feat(launcher): opt-in Rust engine behind the existing launcher (#262…
claude Sep 9, 2026
9f34e20
docs(P-022): CI gate, generated-counts fragment and status surfaces f…
claude Sep 9, 2026
757c29d
test(stage1): make the engine controls load-bearing on both launcher …
claude Sep 9, 2026
1e89d8a
test(stage1): D4.1(b) exposes NEITHER engine's verdict on divergence
claude Sep 9, 2026
063ac62
style(stage1): wrap two over-long lines to satisfy the repo's ruff gate
claude Sep 9, 2026
1543878
evidence(stage1): record the mutation campaign and its generated counts
claude Sep 9, 2026
69c1b50
fix(stage1): make the engine contract hold on Windows, not only on Linux
claude Sep 9, 2026
2dac238
fix(stage1): normalise Windows child status; surface the child's stderr
claude Sep 9, 2026
ceba906
fix(stage1): run own-check.sh through git-bash, not WSL's bash.exe stub
claude Sep 9, 2026
4b45469
fix(stage1): a Windows compare divergence is the declared A/B/C change
claude Sep 9, 2026
e7170cd
fix(stage1): a non-executable candidate is not a state git-bash can b…
claude Sep 9, 2026
0f51868
fix(stage1): own-check.ps1 writes stderr text, not PowerShell ErrorRe…
claude Sep 9, 2026
2d6e43a
evidence(stage1): re-record the campaign against the tree CI proved g…
claude Sep 9, 2026
7ca3655
fix(stage1): five review defects in identity, classification and PS s…
claude Sep 9, 2026
8a68b80
test(stage1): the ps1 spawn seam is a question only Windows can be asked
claude Sep 9, 2026
a565c00
fix(stage1): the shell's absoluteness test matched nothing, and only …
claude Sep 9, 2026
d4b55bd
fix(stage1): own-check.ps1 asked the platform to OPEN the candidate, …
claude Sep 9, 2026
d97102e
fix(stage1): a control that raises is one failed check, not a lost suite
claude Sep 9, 2026
1d649e9
fix(evidence): a campaign definition must hash the same on both platf…
claude Sep 9, 2026
4ccf68d
evidence(stage1): both campaigns recorded against the shipped tree, p…
claude Sep 9, 2026
da897b4
test(stage1): the stderr half of "both streams" was never compared
claude Sep 9, 2026
157b689
evidence(stage1): both campaigns re-recorded, and "both streams" now …
claude Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Shell scripts must arrive with LF endings on every platform.
#
# `scripts/own-check.sh` is executed by git-bash on Windows (#262 Stage 1's
# engine controls run it there). A checkout with the Windows default
# `core.autocrlf=true` would rewrite it to CRLF, and bash cannot run a script
# whose every line ends in a stray carriage return — it fails immediately, with
# an exit code that looks like the script's own. No job had ever run this
# script on a Windows runner before, so the defect was latent rather than new.
#
# Deliberately NARROW. This repository has byte-sensitive evidence (fixtures,
# goldens, recorded campaign results) whose exact bytes are the contract, and a
# blanket `* text=auto` would put those bytes at the mercy of a checkout
# setting. #260 carries "a researched .gitattributes for byte-sensitive
# evidence" as its own hygiene tail; this rule covers executable shell scripts
# only and takes no position on that wider question.
*.sh text eol=lf

# Campaign evidence is hashed BY BYTE, so it must not depend on a checkout
# setting. `mutate_campaign.py` records the sha256 of the definition it ran,
# and the gate refuses a result whose definition hash does not match. The
# #262 Stage-1 Windows campaign runs on a windows-latest runner, where
# `core.autocrlf=true` rewrote these files on checkout: the same definition
# hashed 79ad1476f219 on Linux and 3dda13ce16d6 on Windows, and a run that
# measured exactly the right tree was rejected as evidence for another tree.
docs/evidence/*.json text eol=lf
213 changes: 213 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,219 @@ jobs:
- name: cargo test -p own-cli --features fault-injection (the failure-mode controls)
run: cargo test -p own-cli --features fault-injection --test faults

# P-022 step 8 (#262) STAGE 1 — the launcher's engine-selection contract.
#
# Stage 1 makes the Rust core SELECTABLE while Python stays the default and
# the reference. Every claim in that sentence is a behaviour, so this job
# drives real launchers against a real candidate binary and asserts on the
# observable result. It is deliberately BOTH platforms: the ratified evidence
# is explicit Rust-selected runs on Windows and Linux, and the two differ in
# exactly the mechanics (process launch, executable bits, path forms) that a
# Linux-only job would leave unproven.
#
# OWEN_STAGE1_REQUIRE=1 is the zero-denominator guard: this job exists to
# provide the toolchain, so a control that skips here is a control that did
# not run, and a run of skips would otherwise be indistinguishable from a
# green one.
stage1-engine:
name: owen --engine (#262 Stage 1 controls)
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "8.0.x"
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
- uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master, 2026-07-10
with:
toolchain: stable
# The PRODUCTION candidate. The controls run the real `own-cli`, not a
# stand-in: a control that proves a mock's behaviour proves nothing about
# the binary the launcher will actually spawn.
- name: Build the production own-cli candidate
working-directory: rust
run: cargo build -p own-cli --release
# A second build with #261's off-by-default fault-injection feature, so
# the failure-mode controls force failures through the real binary.
- name: Build the fault-injection own-cli (forced failure modes)
working-directory: rust
run: cargo build -p own-cli --release --features fault-injection --target-dir target-fault
# The controllable native candidate the compare controls need. Built
# with plain `rustc`, deliberately NOT as a cargo workspace member, so it
# cannot move the crate-edge DAG that #261's gate pins.
- name: Build the Stage-1 stub candidate
run: |
ext=""
if [ "${{ matrix.os }}" = "windows-latest" ]; then ext=".exe"; fi
rustc -O tests/helpers/stage1_stub.rs -o "$RUNNER_TEMP/stage1-stub$ext"
- name: Build the owen launcher
run: dotnet build frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj -c Release --nologo
# The vendored Python core is a PACK-time payload, so a plain build does
# not place it where the launcher looks. Staging it here is harness setup
# for the Python-engine paths, not a production behaviour.
- name: Stage the vendored core beside the built launcher
run: |
out=frontend/roslyn/OwnSharp.Cli/bin/Release/net8.0
mkdir -p "$out/ownlang-core/ownlang"
cp ownlang/*.py "$out/ownlang-core/ownlang/"
- name: Stage-1 engine controls (no fail-fast)
env:
OWEN_STAGE1_REQUIRE: "1"
run: |
ext=""
if [ "${{ matrix.os }}" = "windows-latest" ]; then ext=".exe"; fi
export OWEN_RUST_CORE="$PWD/rust/target/release/own-cli$ext"
export OWEN_STAGE1_RUST_FAULT="$PWD/rust/target-fault/release/own-cli$ext"
export OWEN_STAGE1_STUB="$RUNNER_TEMP/stage1-stub$ext"
export OWEN_STAGE1_LAUNCHER_DLL="$PWD/frontend/roslyn/OwnSharp.Cli/bin/Release/net8.0/ownsharp.dll"
python tests/test_stage1_engine.py
# The PowerShell surface's own controls. They run on BOTH legs — the
# logic is platform-neutral and a Linux run catches regressions early —
# but only the Windows leg is evidence for a PowerShell-targeted
# mutation, which is what the stage1-windows-mutations job below settles.
- name: Stage-1 PowerShell controls
env:
OWEN_STAGE1_REQUIRE: "1"
run: |
ext=""
if [ "${{ matrix.os }}" = "windows-latest" ]; then ext=".exe"; fi
export OWEN_RUST_CORE="$PWD/rust/target/release/own-cli$ext"
export OWEN_STAGE1_STUB="$RUNNER_TEMP/stage1-stub$ext"
python tests/test_stage1_ps1.py
# The explicit Rust-selected run on this platform, through the shell
# launcher, recorded as its own step so the evidence names the surface
# and the platform rather than being inferred from a green job.
- name: Explicit Rust-selected run through own-check.sh
run: |
ext=""
if [ "${{ matrix.os }}" = "windows-latest" ]; then ext=".exe"; fi
export OWEN_RUST_CORE="$PWD/rust/target/release/own-cli$ext"
mkdir -p "$RUNNER_TEMP/stage1-sample"
cat > "$RUNNER_TEMP/stage1-sample/Leak.cs" <<'CS'
using System;
using System.IO;
public class Leaky
{
public void Run()
{
var s = new FileStream("x.txt", FileMode.OpenOrCreate);
Console.WriteLine(s.Length);
}
}
CS
set +e
out=$(bash scripts/own-check.sh --engine rust --format human --fail-on-finding -- "$RUNNER_TEMP/stage1-sample")
rc=$?
set -e
echo "$out"
[ "$rc" -eq 1 ] || { echo "FAIL: --engine rust expected exit 1 (findings), got $rc"; exit 1; }
case "$out" in *OWN001*) ;; *) echo "FAIL: no OWN001 finding from the Rust engine"; exit 1 ;; esac
echo "Rust-selected run OK on ${{ matrix.os }}"
# The Windows twin of the shell surface. own-check.ps1 is never exercised
# by the Linux legs, so its engine contract needs its own step here.
- name: own-check.ps1 -Engine rust (Windows only)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
$env:OWEN_RUST_CORE = "$PWD/rust/target/release/own-cli.exe"
$sample = Join-Path $env:RUNNER_TEMP "stage1-sample"
& ./scripts/own-check.ps1 -Engine rust -Format human -FailOnFinding -- $sample
if ($LASTEXITCODE -ne 1) { throw "own-check.ps1 -Engine rust expected exit 1, got $LASTEXITCODE" }
# And the D3.1 seam on this surface: an unusable locator is exit 2.
$env:OWEN_RUST_CORE = "C:\definitely\not\a\binary.exe"
& ./scripts/own-check.ps1 -Engine rust -Format human -- $sample
if ($LASTEXITCODE -ne 2) { throw "own-check.ps1 with a bad OWEN_RUST_CORE expected exit 2, got $LASTEXITCODE" }
Write-Host "own-check.ps1 engine contract OK"
# The assertions above ARE this step's verdict, so say so
# explicitly. GitHub's pwsh wrapper appends `exit $LASTEXITCODE`,
# and the last command here exits 2 ON PURPOSE — without this the
# step would inherit the exit code of a case it was written to
# require.
exit 0

# P-022 step 8 (#262) Stage 1 — the WINDOWS-NATIVE mutation leg.
#
# A mutation whose target is scripts/own-check.ps1 is only `caught` when a
# Windows PowerShell catcher observes the mutant and fails. Running those
# mutants on Linux would execute the mutated PowerShell under a different
# runtime, and the Windows-specific halves — the spawn seam above all —
# cannot be settled there at all: a mutant that runs where its control is
# weakest is decorative, and proves nothing about the surface it edits.
#
# The same is true of one arm of own-check.sh. A drive-rooted path is
# absolute only where Windows resolves it, so a mutant of that arm changes
# nothing a Linux runner can observe, and the Linux campaign would record it
# as caught or survived on the strength of a question it never asked.
#
# So this campaign runs here, on Windows, and this job is the gate. It fails
# unless every mutation is caught with its expected catcher and the
# honesty control survives the unmutated tree.
stage1-windows-mutations:
name: Windows-native mutation campaign (own-check.ps1 + the drive-rooted locator arm)
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "8.0.x"
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
- uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master, 2026-07-10
with:
toolchain: stable
- name: Build the production own-cli candidate
working-directory: rust
run: cargo build -p own-cli --release
- name: Build the Stage-1 stub candidate
run: rustc -O tests/helpers/stage1_stub.rs -o "$RUNNER_TEMP/stage1-stub.exe"
- name: Run the Windows-native mutation campaign
env:
OWEN_STAGE1_REQUIRE: "1"
run: |
export OWEN_RUST_CORE="$PWD/rust/target/release/own-cli.exe"
export OWEN_STAGE1_STUB="$RUNNER_TEMP/stage1-stub.exe"
python scripts/mutate_campaign.py --campaign docs/evidence/p022-stage1-windows.json --run
# The recorded run, printed in full so its provenance and per-mutation
# catchers can be read off this job rather than taken on trust — and so
# the committed record can be reproduced from a named CI run, the way
# #260's sweep record is.
- name: Print the recorded result
if: always()
run: cat docs/evidence/p022-stage1-windows.result.json
- name: Assert every mutation was caught by a Windows catcher
run: |
python - <<'PY'
import json, sys
d = json.load(open("docs/evidence/p022-stage1-windows.result.json", encoding="utf-8"))
defn = json.load(open("docs/evidence/p022-stage1-windows.json", encoding="utf-8"))
exp = {m["id"]: set(m["expected_catchers"]) for m in defn["mutations"]}
problems = []
if d["control"]["outcome"] != "survived":
problems.append("the honesty control did not survive the unmutated tree")
for m in d["mutations"]:
if m["outcome"] != "caught":
problems.append(f"{m['id']}: {m['outcome']}")
elif not exp[m["id"]] <= set(m["catchers"]):
problems.append(f"{m['id']}: expected catchers missed ({m['catchers']})")
print("\n".join(problems) if problems
else f"all {len(d['mutations'])} mutations caught natively on Windows, "
"each by the catcher its definition names")
sys.exit(1 if problems else 0)
PY

# P-022 step 7a (#260) — COMPARE MODE over the committed corpus: the FAST half
# of #260's test matrix, and one leg of it. The five pinned OSS repositories,
# the large-solution controls and the examples tree are the scheduled/manual
Expand Down
23 changes: 21 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ inputs:
description: "How findings are shown: error (default) or warning (advisory)."
required: false
default: "error"
engine:
description: >-
Which analysis engine runs (#262 Stage 1): python (the DEFAULT and the
reference), rust (the Rust core `own-cli ownir`), or compare (both over
one captured input, exposing the reference's result only when they agree
byte for byte). rust and compare require the candidate binary's absolute
path in the OWEN_RUST_CORE environment variable — the Action does no
discovery, so an unset or unusable OWEN_RUST_CORE is a configuration
error (exit 2) and never a silent fall back to Python. compare is a
development/CI seam for the migration, not yet a promised feature.
required: false
default: "python"
fail-on-finding:
description: >-
Whether a FINDING fails the step. Default false: findings are published
Expand Down Expand Up @@ -89,10 +101,15 @@ runs:
OWN_PATH: ${{ inputs.path }}
OWN_FORMAT: ${{ inputs.format }}
OWN_SEVERITY: ${{ inputs.severity }}
OWN_ENGINE: ${{ inputs.engine }}
OWN_FAIL_ON_FINDING: ${{ inputs.fail-on-finding }}
OWN_SARIF_FILE: ${{ inputs.sarif-file }}
OWN_CONFIG: ${{ inputs.config }}
run: |
# D2: the Action is one of the four launcher surfaces, but its
# engine-selection SEMANTICS are own-check.sh's — it delegates rather
# than re-implementing them, so the real fan-out is smaller than four
# and there is exactly one contract to keep true.
check="${{ github.action_path }}/scripts/own-check.sh"
# P-035: forward an explicit own.toml to own-check when the caller set one.
# Passed as data via OWN_CONFIG (never interpolated into the script body).
Expand All @@ -112,7 +129,8 @@ runs:
sarif="${OWN_SARIF_FILE:-$RUNNER_TEMP/owen.sarif}"
set +e
"$check" --root "${{ github.action_path }}" --format sarif \
--severity "$OWN_SEVERITY" "${config_args[@]}" --fail-on-finding -- "$OWN_PATH" > "$sarif"
--severity "$OWN_SEVERITY" --engine "$OWN_ENGINE" "${config_args[@]}" \
--fail-on-finding -- "$OWN_PATH" > "$sarif"
rc=$?
set -e
echo "sarif-file=$sarif" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -143,7 +161,8 @@ runs:
# action is allowed to negotiate about.
set +e
"$check" --root "${{ github.action_path }}" --format "$OWN_FORMAT" \
--severity "$OWN_SEVERITY" "${config_args[@]}" --fail-on-finding -- "$OWN_PATH"
--severity "$OWN_SEVERITY" --engine "$OWN_ENGINE" "${config_args[@]}" \
--fail-on-finding -- "$OWN_PATH"
rc=$?
set -e
if [ "$rc" -ge 2 ]; then
Expand Down
Loading
Loading