diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ceafac49..b6d13f7c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1717,15 +1717,15 @@ jobs:
run: |
# The contract GitHub's code-scanning ingest enforces, checked locally so
# the upload (own-check-codescan job) is never the first place a drift is
- # found: a single-run 2.1.0 log, the Own.NET driver, and every result
+ # found: a single-run 2.1.0 log, the Owen driver, and every result
# carrying a catalogue ruleId + a located file. No upload, no permissions.
out="$RUNNER_TEMP/own.sarif"
scripts/own-check.sh --format sarif --severity warning -- frontend/roslyn/samples > "$out"
echo "wrote $(wc -c < "$out") bytes"
jq -e '.version == "2.1.0" and ((.runs | length) == 1)' "$out" >/dev/null \
|| { echo "FAIL: not a single-run SARIF 2.1.0 log"; exit 1; }
- jq -e '.runs[0].tool.driver.name == "Own.NET"' "$out" >/dev/null \
- || { echo "FAIL: tool.driver.name is not Own.NET"; exit 1; }
+ jq -e '.runs[0].tool.driver.name == "Owen"' "$out" >/dev/null \
+ || { echo "FAIL: tool.driver.name is not Owen"; exit 1; }
# A dangling ruleId or an unlocated result is the #1 reason GitHub rejects
# a SARIF; startLine is optional (a file-level finding omits it -> // 1).
jq -e '
@@ -1739,7 +1739,7 @@ jobs:
and ((.locations[0].physicalLocation.region.startLine // 1) | type == "number"))
' "$out" >/dev/null \
|| { echo "FAIL: a result is unlocated or references an undeclared rule"; exit 1; }
- echo "OK: SARIF 2.1.0 — Own.NET driver, every result rule-backed + located"
+ echo "OK: SARIF 2.1.0 — Owen driver, every result rule-backed + located"
- name: The composite action runs end-to-end (non-failing)
uses: ./
with:
@@ -1942,7 +1942,10 @@ jobs:
# Alpha gate A (issue #202): the single delightful command, proven end-to-end
# on a clean runner — install -> check -> findings. Packaging only, no
- # analysis-behaviour change: OwnSharp.Cli bundles the *unmodified* extractor
+ # analysis-behaviour change: the underlying project stays OwnSharp.Cli
+ # internally (P-013; not mass-renamed), but the PUBLISHED identity is the
+ # Owen public facade (docs/notes/owen-public-facade.md) — package ID
+ # Owen.Cli, command `owen`. Bundles the *unmodified* extractor
# (ProjectReference; invoked as a child process, same shape own-check.sh
# already uses) and vendors the *unmodified* ownlang/ core, run by the
# machine's own Python. Both ubuntu AND windows matter here specifically
@@ -1950,7 +1953,7 @@ jobs:
# Windows and a shell script on Unix, so they exercise genuinely different
# process-launch mechanics; ubuntu-only would not prove the Windows path.
ownsharp-cli-smoke:
- name: ownsharp CLI (gate A) — clean install -> check -> findings
+ name: owen CLI (gate A) — clean install -> check -> findings
strategy:
fail-fast: false
matrix:
@@ -1959,7 +1962,7 @@ jobs:
defaults:
run:
# bash (git-bash on Windows runners) so one script works on both legs;
- # the thing under test is the ownsharp/dotnet/python binaries, not the
+ # the thing under test is the owen/dotnet/python binaries, not the
# shell driving them.
shell: bash
steps:
@@ -1976,14 +1979,31 @@ jobs:
# published there yet, see P-013's Non-goals) -- pack from the source
# this job already checked out. Deliberately OUTSIDE the timed window
# below: it is not part of the "install -> check" claim being proven.
- - name: Pack OwnSharp.Cli (pulls in the extractor via ProjectReference)
- run: dotnet pack frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj -c Release -o "$RUNNER_TEMP/ownsharp-nupkg"
- - name: A minimal leak, in a scratch dir OUTSIDE the repo
+ - name: Pack Owen.Cli (pulls in the extractor via ProjectReference)
+ run: dotnet pack frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj -c Release -o "$RUNNER_TEMP/owen-nupkg"
+ # --add-source alone is not enough (Codex review, PR #244): `dotnet tool
+ # install` queries every configured source (nuget.org included) IN
+ # PARALLEL and takes whichever answers first — once a same-numbered
+ # version is ever actually on nuget.org, this install could silently
+ # resolve from there instead of the just-packed artifact. An isolated
+ # nuget.config with removes the ambiguity.
+ - name: Isolated NuGet.config — the packed artifact is the ONLY visible source
+ run: |
+ cat > "$RUNNER_TEMP/isolated-nuget.config" <
+
+
+
+
+
+
+ EOF
+ - name: Seed a leak sample, a clean sample, and an unsupported-input sample -- all OUTSIDE the repo
# Proves the tool needs nothing but itself + Python -- not the Own.NET
# checkout (a real end user obviously won't have this repo on disk).
run: |
- mkdir -p "$RUNNER_TEMP/ownsharp-sample"
- cat > "$RUNNER_TEMP/ownsharp-sample/Leak.cs" <<'EOF'
+ mkdir -p "$RUNNER_TEMP/owen-sample" "$RUNNER_TEMP/owen-clean" "$RUNNER_TEMP/owen-unsupported"
+ cat > "$RUNNER_TEMP/owen-sample/Leak.cs" <<'EOF'
using System.IO;
public class Leaky
{
@@ -1994,19 +2014,90 @@ jobs:
}
}
EOF
+ cat > "$RUNNER_TEMP/owen-clean/Clean.cs" <<'EOF'
+ using System.IO;
+ public class Tidy
+ {
+ public void Run()
+ {
+ using var s = new MemoryStream();
+ s.WriteByte(1);
+ }
+ }
+ EOF
+ cat > "$RUNNER_TEMP/owen-unsupported/app.ts" <<'EOF'
+ console.log("not C# -- the .NET/C# frontend is the only one wired in today");
+ EOF
- name: Start the clean-machine timer (install -> check -> findings)
run: echo "SMOKE_START=$(date +%s)" >> "$GITHUB_ENV"
- - name: dotnet tool install --global (the one install the user runs)
- run: dotnet tool install --global OwnSharp.Cli --version 0.1.0 --add-source "$RUNNER_TEMP/ownsharp-nupkg"
- - name: ownsharp check finds the leak
+ - name: dotnet tool install --global (the one install the user runs) -- pins the package ID
+ run: dotnet tool install --global Owen.Cli --version 0.1.0 --configfile "$RUNNER_TEMP/isolated-nuget.config"
+ - name: owen --help -- language-neutral product framing, explicit included-frontend list, no TypeScript claim
+ run: |
+ out=$(owen --help)
+ echo "$out"
+ echo "$out" | grep -qi "^owen " || { echo "FAIL: --help should open with the owen product line"; exit 1; }
+ echo "$out" | grep -q "Included frontend" || { echo "FAIL: --help should list the included frontend explicitly"; exit 1; }
+ echo "$out" | grep -q '\.cs, \.csproj, \.sln' || { echo "FAIL: --help should name the .NET/C# frontend's extensions"; exit 1; }
+ if echo "$out" | grep -qi "typescript"; then
+ echo "FAIL: --help must not claim TypeScript support before it's actually wired in"; exit 1
+ fi
+ - name: owen --version -- pins the command name via PATH resolution alone
+ run: |
+ out=$(owen --version)
+ [ -n "$out" ] || { echo "FAIL: --version printed nothing"; exit 1; }
+ echo "OK: owen --version -> $out"
+ - name: "owen -- prefix is owen:, not the pre-rebrand ownsharp:"
+ run: |
+ set +e
+ out=$(owen bogus-command 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -ge 2 ] || { echo "FAIL: expected a non-zero exit for an unknown command, got $rc"; exit 1; }
+ echo "$out" | grep -q "^owen: unknown command" || { echo "FAIL: expected an 'owen: unknown command' prefix"; exit 1; }
+ - name: owen check finds the leak (installed execution, outside any checkout)
run: |
set +e
- out=$(ownsharp check "$RUNNER_TEMP/ownsharp-sample" --fail-on-finding 2>&1)
+ out=$(owen check "$RUNNER_TEMP/owen-sample" --fail-on-finding 2>&1)
rc=$?
set -e
echo "$out"
[ "$rc" -eq 1 ] || { echo "FAIL: expected exit 1 (findings), got $rc"; exit 1; }
echo "$out" | grep -q "OWN001" || { echo "FAIL: expected OWN001 in the output"; exit 1; }
+ - name: owen check on clean code exits 0 (negative control)
+ run: |
+ set +e
+ out=$(owen check "$RUNNER_TEMP/owen-clean" --fail-on-finding 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 0 ] || { echo "FAIL: expected exit 0 on clean code, got $rc"; exit 1; }
+ - name: owen check --format sarif -- Owen-branded SARIF driver name
+ run: |
+ out=$(owen check "$RUNNER_TEMP/owen-sample" --format sarif)
+ echo "$out" | python -c "
+ import json, sys
+ d = json.load(sys.stdin)
+ n = d['runs'][0]['tool']['driver']['name']
+ assert n == 'Owen', f'SARIF driver name is {n!r}, expected Owen'
+ print('OK: SARIF driver name is Owen')
+ "
+ - name: owen check on unsupported input fails explicitly -- never a silent clean scan
+ run: |
+ set +e
+ out=$(owen check "$RUNNER_TEMP/owen-unsupported" 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 4 ] || { echo "FAIL: expected exit 4 (no supported input), got $rc"; exit 1; }
+ echo "$out" | grep -qi "no supported input" || { echo "FAIL: expected an explicit unsupported-input message"; exit 1; }
+ # The exact phrase a genuinely clean C# scan would print (own-check's
+ # core render, e.g. "0 findings.") must NOT appear here -- that would
+ # mean unsupported input silently looked like a successful clean scan.
+ if echo "$out" | grep -q "^0 findings\.$"; then
+ echo "FAIL: must not report a clean 0-findings scan for unsupported input"; exit 1
+ fi
- name: Stop the timer -- report it, and gate on a generous regression ceiling
# A hard ceiling, not a precision claim: CI timing varies with runner
# load, so this is a regression guard (catch "it now takes 20 minutes"),
@@ -2015,14 +2106,215 @@ jobs:
elapsed=$(( $(date +%s) - SMOKE_START ))
echo "install -> check -> findings: ${elapsed}s"
[ "$elapsed" -lt 240 ] || { echo "FAIL: took ${elapsed}s (ceiling 240s) — see alpha-readiness.md gate A"; exit 1; }
- - name: No Python found -> a fast, actionable failure (never an auto-download)
+ - name: No Python found via OWEN_PYTHON -> a fast, actionable failure (never an auto-download)
run: |
set +e
- out=$(OWN_PYTHON=/definitely/does/not/exist/python3 ownsharp check "$RUNNER_TEMP/ownsharp-sample" 2>&1)
+ out=$(OWEN_PYTHON=/definitely/does/not/exist/python3 owen check "$RUNNER_TEMP/owen-sample" 2>&1)
rc=$?
set -e
echo "$out"
[ "$rc" -eq 3 ] || { echo "FAIL: expected exit 3 (Python not found), got $rc"; exit 1; }
- echo "$out" | grep -qi "OWN_PYTHON" || { echo "FAIL: expected the OWN_PYTHON-specific message"; exit 1; }
+ echo "$out" | grep -q "OWEN_PYTHON" || { echo "FAIL: expected the OWEN_PYTHON-specific message"; exit 1; }
echo "$out" | grep -Eiq "winget|apt|brew|python.org" || { echo "FAIL: expected an actionable install hint"; exit 1; }
+ - name: Legacy OWN_PYTHON still works as a temporary fallback, with a deprecation note
+ run: |
+ own_python="$(command -v python3 || command -v python)"
+ [ -n "$own_python" ] || { echo "FAIL: could not find a python3/python on PATH to test the fallback with"; exit 1; }
+ set +e
+ out=$(OWN_PYTHON="$own_python" owen check "$RUNNER_TEMP/owen-sample" --fail-on-finding 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 1 ] || { echo "FAIL: expected exit 1 (findings, via the legacy var), got $rc"; exit 1; }
+ echo "$out" | grep -qi "OWN_PYTHON is deprecated" || { echo "FAIL: expected a deprecation note when OWN_PYTHON is used"; exit 1; }
+ echo "$out" | grep -q "OWN001" || { echo "FAIL: expected OWN001 in the output even via the legacy var"; exit 1; }
+ - name: Seed a same-content legacy cache and confirm it is reused (no ~/.owen copy)
+ # Review, PR #246 -- the fallback must trust DESTINATION content, not a
+ # marker's say-so. This is the legitimate case: exact byte-for-byte
+ # match with the just-packed core, reused in place.
+ run: |
+ rm -rf "$HOME/.owen" "$HOME/.ownsharp"
+ mkdir -p "$HOME/.ownsharp/core/0.1.0/ownlang"
+ cp ownlang/*.py "$HOME/.ownsharp/core/0.1.0/ownlang/"
+ driver=$(owen check "$RUNNER_TEMP/owen-sample" --format sarif | python -c "import json,sys; print(json.load(sys.stdin)['runs'][0]['tool']['driver']['name'])")
+ [ "$driver" = "Owen" ] || { echo "FAIL: driver name '$driver' via the legacy cache"; exit 1; }
+ if [ -d "$HOME/.owen" ]; then
+ echo "FAIL: ~/.owen was created despite a legitimate matching legacy cache (fallback not reused in place)"; exit 1
+ fi
+ echo "OK: matching legacy cache reused in place, no unnecessary copy"
+ - name: Seed a legacy cache with an EXTRA (removed) file and confirm it is REJECTED
+ # The exact reproduction from review: an old cache holds a file the new
+ # source no longer has. A version-only marker would have missed this;
+ # the content fingerprint must not.
+ run: |
+ rm -rf "$HOME/.owen" "$HOME/.ownsharp"
+ mkdir -p "$HOME/.ownsharp/core/0.1.0/ownlang"
+ cp ownlang/*.py "$HOME/.ownsharp/core/0.1.0/ownlang/"
+ echo "# stale leftover module" > "$HOME/.ownsharp/core/0.1.0/ownlang/removed_module.py"
+ driver=$(owen check "$RUNNER_TEMP/owen-sample" --format sarif | python -c "import json,sys; print(json.load(sys.stdin)['runs'][0]['tool']['driver']['name'])")
+ [ "$driver" = "Owen" ] || { echo "FAIL: driver name '$driver'"; exit 1; }
+ [ -d "$HOME/.owen" ] || { echo "FAIL: expected a fresh ~/.owen unpack (extra-file legacy cache should have been rejected)"; exit 1; }
+ if find "$HOME/.owen" -name "removed_module.py" | grep -q .; then
+ echo "FAIL: the stale extra file leaked into the fresh cache"; exit 1
+ fi
+ echo "OK: extra-file legacy cache correctly rejected, fresh cache is clean"
+ - name: Seed a legacy cache with MODIFIED content (same filenames, different bytes) and confirm rejection
+ # Same version, same file SET, different CONTENT -- the same-version
+ # class of bug this whole fingerprint mechanism exists to catch.
+ run: |
+ rm -rf "$HOME/.owen" "$HOME/.ownsharp"
+ mkdir -p "$HOME/.ownsharp/core/0.1.0/ownlang"
+ cp ownlang/*.py "$HOME/.ownsharp/core/0.1.0/ownlang/"
+ echo "# tampered" >> "$HOME/.ownsharp/core/0.1.0/ownlang/ownir.py"
+ driver=$(owen check "$RUNNER_TEMP/owen-sample" --format sarif | python -c "import json,sys; print(json.load(sys.stdin)['runs'][0]['tool']['driver']['name'])")
+ [ "$driver" = "Owen" ] || { echo "FAIL: driver name '$driver'"; exit 1; }
+ [ -d "$HOME/.owen" ] || { echo "FAIL: expected a fresh ~/.owen unpack (modified-content legacy cache should have been rejected)"; exit 1; }
+ echo "OK: modified-content legacy cache correctly rejected"
+ - name: Directory containing only skipped files (bin/obj) -- exit 4, not a silent clean scan
+ run: |
+ rm -rf "$HOME/.owen" "$HOME/.ownsharp"
+ mkdir -p "$RUNNER_TEMP/owen-skip-only/bin" "$RUNNER_TEMP/owen-skip-only/obj"
+ cat > "$RUNNER_TEMP/owen-skip-only/bin/Ignored.cs" <<'EOF'
+ public class X { public void M() { var s = new System.IO.MemoryStream(); } }
+ EOF
+ cat > "$RUNNER_TEMP/owen-skip-only/obj/AlsoIgnored.cs" <<'EOF'
+ public class Y { public void M() { var s = new System.IO.MemoryStream(); } }
+ EOF
+ set +e
+ out=$(owen check "$RUNNER_TEMP/owen-skip-only" 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 4 ] || { echo "FAIL: expected exit 4 (bin/obj-only directory), got $rc"; exit 1; }
+ echo "$out" | grep -qi "no supported input" || { echo "FAIL: expected an explicit unsupported-input message"; exit 1; }
+ - name: Directory containing only generated files (*.g.cs) -- exit 4
+ run: |
+ mkdir -p "$RUNNER_TEMP/owen-generated-only"
+ cat > "$RUNNER_TEMP/owen-generated-only/Foo.g.cs" <<'EOF'
+ public class Gen { public void M() { var s = new System.IO.MemoryStream(); } }
+ EOF
+ set +e
+ out=$(owen check "$RUNNER_TEMP/owen-generated-only" 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 4 ] || { echo "FAIL: expected exit 4 (generated-only directory), got $rc"; exit 1; }
+ echo "$out" | grep -qi "no supported input" || { echo "FAIL: expected an explicit unsupported-input message"; exit 1; }
+ - name: Empty .csproj (no .cs files in its directory) -- exit 4
+ run: |
+ mkdir -p "$RUNNER_TEMP/owen-empty-proj"
+ cat > "$RUNNER_TEMP/owen-empty-proj/Empty.csproj" <<'EOF'
+
+ net8.0
+
+ EOF
+ set +e
+ out=$(owen check "$RUNNER_TEMP/owen-empty-proj/Empty.csproj" 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 4 ] || { echo "FAIL: expected exit 4 (empty .csproj), got $rc"; exit 1; }
+ - name: .sln with no usable projects -- exit 4
+ run: |
+ mkdir -p "$RUNNER_TEMP/owen-empty-sln"
+ cat > "$RUNNER_TEMP/owen-empty-sln/Empty.sln" <<'EOF'
+ Microsoft Visual Studio Solution File, Format Version 12.00
+ # no Project( lines at all
+ EOF
+ set +e
+ out=$(owen check "$RUNNER_TEMP/owen-empty-sln/Empty.sln" 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 4 ] || { echo "FAIL: expected exit 4 (.sln with no projects), got $rc"; exit 1; }
+ - name: Skipped files plus one real source -- the real one is still found, exit 1
+ run: |
+ mkdir -p "$RUNNER_TEMP/owen-mixed-skip/bin" "$RUNNER_TEMP/owen-mixed-skip/src"
+ cat > "$RUNNER_TEMP/owen-mixed-skip/bin/Ignored.cs" <<'EOF'
+ public class X { public void M() { var s = new System.IO.MemoryStream(); } }
+ EOF
+ cat > "$RUNNER_TEMP/owen-mixed-skip/src/Real.cs" <<'EOF'
+ using System.IO;
+ public class Leaky { public void Run() { var s = new MemoryStream(); s.WriteByte(1); } }
+ EOF
+ set +e
+ out=$(owen check "$RUNNER_TEMP/owen-mixed-skip" --fail-on-finding 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 1 ] || { echo "FAIL: expected exit 1 (real source found despite bin/ noise), got $rc"; exit 1; }
+ echo "$out" | grep -q "OWN001" || { echo "FAIL: expected OWN001"; exit 1; }
+ - name: Inaccessible subtree plus one readable source -- tolerated, real source still found
+ # On these hosted Linux runners the job does not run as an all-powerful
+ # admin/root account, so chmod genuinely restricts access (unlike a
+ # root-based local sandbox, where this same scenario cannot be exercised,
+ # and unlike Windows runners, where chmod 000 does not reliably lock out
+ # the job's own account -- review, PR #246: restricted to Linux so the
+ # assertions below are actually exercising the locked path, not silently
+ # passing because both files got analyzed).
+ if: runner.os == 'Linux'
+ run: |
+ mkdir -p "$RUNNER_TEMP/owen-mixed-locked/locked" "$RUNNER_TEMP/owen-mixed-locked/readable"
+ cat > "$RUNNER_TEMP/owen-mixed-locked/locked/Secret.cs" <<'EOF'
+ public class X { public void M() { var s = new System.IO.MemoryStream(); } }
+ EOF
+ cat > "$RUNNER_TEMP/owen-mixed-locked/readable/Real.cs" <<'EOF'
+ using System.IO;
+ public class Leaky { public void Run() { var s = new MemoryStream(); s.WriteByte(1); } }
+ EOF
+ chmod 000 "$RUNNER_TEMP/owen-mixed-locked/locked"
+ set +e
+ out=$(owen check "$RUNNER_TEMP/owen-mixed-locked" --fail-on-finding 2>&1)
+ rc=$?
+ set -e
+ chmod 755 "$RUNNER_TEMP/owen-mixed-locked/locked"
+ echo "$out"
+ if echo "$out" | grep -qi "UnauthorizedAccess\|Unhandled exception"; then
+ echo "FAIL: crashed on the locked subdirectory instead of tolerating it"; exit 1
+ fi
+ [ "$rc" -eq 1 ] || { echo "FAIL: expected exit 1 (the readable source's leak), got $rc"; exit 1; }
+ echo "$out" | grep -q "Real.cs" || { echo "FAIL: readable source was not analyzed"; exit 1; }
+ if echo "$out" | grep -q "Secret.cs"; then
+ echo "FAIL: the inaccessible source was unexpectedly analyzed -- chmod 000 did not actually lock it out, so this run proves nothing"; exit 1
+ fi
+ - name: Uppercase extensions are accepted case-insensitively (Foo.CS)
+ run: |
+ mkdir -p "$RUNNER_TEMP/owen-uppercase"
+ cat > "$RUNNER_TEMP/owen-uppercase/Leak.CS" <<'EOF'
+ using System.IO;
+ public class Leaky { public void Run() { var s = new MemoryStream(); s.WriteByte(1); } }
+ EOF
+ set +e
+ out=$(owen check "$RUNNER_TEMP/owen-uppercase/Leak.CS" --fail-on-finding 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 1 ] || { echo "FAIL: expected exit 1 for uppercase .CS extension, got $rc"; exit 1; }
+ echo "$out" | grep -q "OWN001" || { echo "FAIL: expected OWN001"; exit 1; }
+ - name: A tampered CURRENT (fingerprint-named) cache is rejected and rebuilt
+ # Review, PR #246 round 4 -- the earlier fixes verified a LEGACY cache's
+ # actual content, but a hit at the current ~/.owen/core///
+ # path itself was still trusted on existence alone. Reproduction: run once
+ # to create it, then tamper the file content directly under that exact
+ # fingerprint-named path (not the legacy location) and add a stale extra
+ # file, then confirm the second run rejects it, rebuilds cleanly, and the
+ # stale file is gone from whatever cache directory actually got used.
+ run: |
+ rm -rf "$HOME/.owen" "$HOME/.ownsharp"
+ owen check "$RUNNER_TEMP/owen-sample" --fail-on-finding > /dev/null 2>&1 || true
+ cache_dir=$(find "$HOME/.owen/core" -mindepth 2 -maxdepth 2 -type d)
+ [ -n "$cache_dir" ] || { echo "FAIL: first run did not create a current-cache directory"; exit 1; }
+ echo "# tampered" >> "$cache_dir/ownlang/ownir.py"
+ echo "# stale leftover module" > "$cache_dir/ownlang/stale_module.py"
+ driver=$(owen check "$RUNNER_TEMP/owen-sample" --format sarif | python -c "import json,sys; print(json.load(sys.stdin)['runs'][0]['tool']['driver']['name'])")
+ [ "$driver" = "Owen" ] || { echo "FAIL: driver name '$driver' after rebuild"; exit 1; }
+ if find "$HOME/.owen" -name "stale_module.py" | grep -q .; then
+ echo "FAIL: the stale extra file survived under a used cache directory"; exit 1
+ fi
+ if grep -q "# tampered" "$cache_dir/ownlang/ownir.py" 2>/dev/null; then
+ echo "FAIL: the tampered file content is still being served from the original path"; exit 1
+ fi
+ echo "OK: tampered current-cache destination rejected and rebuilt clean"
+ - name: Clean up cache state left by the edge-case tests above
+ run: rm -rf "$HOME/.owen" "$HOME/.ownsharp"
diff --git a/README.md b/README.md
index 459f6eac..2ad5b3f8 100644
--- a/README.md
+++ b/README.md
@@ -27,8 +27,9 @@ scripts/own-check.sh --format human -- /path/to/your/csharp/repo
```
Needs Python 3.11+ and the .NET SDK on `PATH` — nothing to build, nothing to
-`pip install`. A packaged single-command CLI (`ownsharp check`) also exists —
-build-and-install-locally today, not yet published to nuget.org; see
+`pip install`. A packaged single-command CLI (`owen check`, package
+`Owen.Cli`) also exists — build-and-install-locally today, not yet published
+to nuget.org; see
[`frontend/roslyn/OwnSharp.Cli/README.md`](frontend/roslyn/OwnSharp.Cli/README.md)
and [`docs/notes/alpha-readiness.md`](docs/notes/alpha-readiness.md) gate **A**.
@@ -921,7 +922,7 @@ ownlang/
test_ownir.py # the OwnIR bridge: C# facts -> core -> OWN001 at the C# site
frontend/roslyn/ # the C# extractor (Roslyn, CI-only) + .cs samples (P-001)
OwnSharp.Extractor/ # ownsharp-extract (dotnet tool): facts only
- OwnSharp.Cli/ # ownsharp (dotnet tool, gate A): extractor + vendored core, one install
+ OwnSharp.Cli/ # owen / Owen.Cli (dotnet tool, gate A): extractor + vendored core, one install
rust/ # the Rust core migration (P-022): own-ir + own-syntax so far,
# oracle-gated against this Python core — see rust/README.md
pyproject.toml # gate: ruff + mypy --strict (see below)
diff --git a/README.ru.md b/README.ru.md
index ec3badef..5160ed94 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -28,8 +28,9 @@ scripts/own-check.sh --format human -- /путь/к/вашему/csharp/репо
```
Нужны Python 3.11+ и .NET SDK в `PATH` — ничего собирать, ничего ставить через
-`pip install`. Есть и упакованный однокомандный CLI (`ownsharp check`) — сегодня
-собирается и ставится локально, в nuget.org ещё не опубликован; см.
+`pip install`. Есть и упакованный однокомандный CLI (`owen check`, пакет
+`Owen.Cli`) — сегодня собирается и ставится локально, в nuget.org ещё не
+опубликован; см.
[`frontend/roslyn/OwnSharp.Cli/README.md`](frontend/roslyn/OwnSharp.Cli/README.md)
и [`docs/notes/alpha-readiness.md`](docs/notes/alpha-readiness.md), gate **A**.
diff --git a/action.yml b/action.yml
index 23d1113e..89c62e12 100644
--- a/action.yml
+++ b/action.yml
@@ -1,4 +1,4 @@
-name: "Own.NET resource-leak check"
+name: "Owen lifetime/resource check"
description: >-
Scan C# for lifetime/resource leaks the compiler cannot express — event/timer
subscription leaks, undisposed IDisposable fields/locals, ignored Subscribe()
@@ -29,7 +29,7 @@ inputs:
required: false
default: "true"
python-version:
- description: "Python version for the Own.NET core."
+ description: "Python version for the Owen core."
required: false
default: "3.13"
dotnet-version:
@@ -39,7 +39,7 @@ inputs:
sarif-file:
description: >-
Where to write the SARIF log when format: sarif (default:
- $RUNNER_TEMP/own-net.sarif). The chosen path is echoed back as the
+ $RUNNER_TEMP/owen.sarif). The chosen path is echoed back as the
sarif-file output regardless.
required: false
default: ""
@@ -54,7 +54,7 @@ outputs:
runs:
using: "composite"
steps:
- - name: Set up Python (Own.NET core)
+ - name: Set up Python (Owen core)
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
@@ -64,7 +64,7 @@ runs:
with:
dotnet-version: ${{ inputs.dotnet-version }}
- - name: Own.NET leak check
+ - name: Owen leak check
id: own
shell: bash
env:
@@ -88,16 +88,16 @@ runs:
# passed so the true exit code (0 clean / 1 findings / >=2 hard error) is
# captured *after* the file is written; whether a finding fails the step
# is the action's own fail-on-finding (default: let code scanning gate).
- sarif="${OWN_SARIF_FILE:-$RUNNER_TEMP/own-net.sarif}"
+ sarif="${OWN_SARIF_FILE:-$RUNNER_TEMP/owen.sarif}"
set +e
"$check" --root "${{ github.action_path }}" --format sarif \
--severity "$OWN_SEVERITY" --fail-on-finding -- "$OWN_PATH" > "$sarif"
rc=$?
set -e
echo "sarif-file=$sarif" >> "$GITHUB_OUTPUT"
- echo "Own.NET wrote SARIF to $sarif ($(wc -c < "$sarif" 2>/dev/null || echo 0) bytes; own-check rc=$rc)"
+ echo "Owen wrote SARIF to $sarif ($(wc -c < "$sarif" 2>/dev/null || echo 0) bytes; own-check rc=$rc)"
if [ "$rc" -ge 2 ]; then
- echo "::error::Own.NET hard error (bad facts / drifted contract)"
+ echo "::error::Owen hard error (bad facts / drifted contract)"
cat "$sarif" >&2 || true
exit "$rc"
fi
@@ -107,7 +107,7 @@ runs:
# fails the redirection (exit 1, outside own-check's 0/1/>=2 contract).
# Surface it; never let it pass as a gated-off finding with no SARIF.
if [ ! -s "$sarif" ]; then
- echo "::error::Own.NET produced no SARIF at $sarif (own-check rc=$rc) — the run or its redirection failed"
+ echo "::error::Owen produced no SARIF at $sarif (own-check rc=$rc) — the run or its redirection failed"
exit 1
fi
if [ "$OWN_FAIL_ON_FINDING" = "true" ] && [ "$rc" -eq 1 ]; then
diff --git a/docs/notes/alpha-readiness.md b/docs/notes/alpha-readiness.md
index 8ab90640..ba6f290a 100644
--- a/docs/notes/alpha-readiness.md
+++ b/docs/notes/alpha-readiness.md
@@ -36,7 +36,7 @@ The bar for "showable": a person can reproduce the wow in ~3 minutes
| | Item | Status (2026-06-27) | Gap to close |
|---|------|--------------------|--------------|
-| **A** | `dotnet tool` one-command CLI | ◑ **mostly built** (issue #202) — `OwnSharp.Cli` wraps extractor+core into one `dotnet tool install` → `ownsharp check `, proven install→check→findings on a clean ubuntu/windows runner in CI (`ownsharp-cli-smoke`). See [`frontend/roslyn/OwnSharp.Cli/README.md`](../../frontend/roslyn/OwnSharp.Cli/README.md). | Not published to nuget.org yet — today it's build-and-install-from-source only. Publishing (+ a real version scheme beyond `0.1.0`) is the remaining step. |
+| **A** | `dotnet tool` one-command CLI | ◑ **mostly built** (issue #202) — `OwnSharp.Cli` (public facade: package `Owen.Cli`, command `owen`) wraps extractor+core into one `dotnet tool install` → `owen check `, proven install→check→findings on a clean ubuntu/windows runner in CI (`ownsharp-cli-smoke`). See [`frontend/roslyn/OwnSharp.Cli/README.md`](../../frontend/roslyn/OwnSharp.Cli/README.md). | Not published to nuget.org yet — today it's build-and-install-from-source only. Publishing (+ a real version scheme beyond `0.1.0`) is the remaining step. |
| **B** | GitHub Action | ✅ **built** — `action.yml`: `path`/`severity`/`format` (`github` / `msbuild` / `human` / `sarif`), purple shield branding. Matches the "stupidly simple YAML" bar. | Publish to Marketplace; pin the 6-line usage in the README. |
| **C** | SARIF / PR annotations | ✅ **built** — SARIF 2.1.0 + GitHub annotations + reachability/evidence (P-015). | — |
| **D** | 5 core diagnostics | ✅ **built, well past** — OWN001/002/003, OWN014, DI001–005, POOL001–005, WPF001–005 (catalog). The comment's `SUB001/SUB002/TMR001/DISP001/DI001` all exist *semantically*; the `SUB/TMR/DISP` catalog rename is the deferred consolidation item, not new work. | (naming only) land the catalog rename with the OwnIR-v1/profile-label work. |
@@ -58,13 +58,13 @@ suppression → "why not Sonar/CodeQL"), every step of that path now exists.
front door, and now A have all closed too — what's left is publishing, not
building:**
-1. ~~a single `ownsharp check MyApp.sln` tool (**A**)~~ — **built**, not yet published to nuget.org;
+1. ~~a single `owen check MyApp.sln` tool (**A**)~~ — **built**, not yet published to nuget.org;
2. ~~a wedge landing README + copy-paste quickstart (front door)~~ — **done**;
3. ~~three packaged case studies from finds we already have (**F**)~~ — **done**;
4. ~~one consolidated suppression / false-positive page (**G**)~~ — **done**.
None of those is research; all are the difference between "interesting PoC" and
-"people install it." Publishing `OwnSharp.Cli` to nuget.org is now the one item
+"people install it." Publishing `Owen.Cli` to nuget.org is now the one item
standing between here and the day 1–30 milestone being *literally* copy-paste
for a stranger.
@@ -87,7 +87,7 @@ until the .NET alpha above is delicious. Do not let the spike exceed 20%.
## 90-day shape (sequencing, not a schedule)
-- **Days 1–30 — make the .NET alpha tasty:** publish `OwnSharp.Cli` to
+- **Days 1–30 — make the .NET alpha tasty:** publish `Owen.Cli` to
nuget.org (A/B/C/D/E/F/G and the README front door are all otherwise done).
Suppression UX + bad/ok corpus polish continue as bug-driven follow-ups, not
a blocking gate.
diff --git a/docs/notes/owen-public-facade.md b/docs/notes/owen-public-facade.md
new file mode 100644
index 00000000..22a336a1
--- /dev/null
+++ b/docs/notes/owen-public-facade.md
@@ -0,0 +1,259 @@
+# Owen: public facade rebrand
+
+This note records a **public-facing rebrand**, not an internal refactor. The
+underlying engine, project names, namespaces, and diagnostic codes are all
+unchanged; only what an external user *sees* — the package name, the CLI
+command, help/error text, the SARIF tool identity, the cache directory, and
+the Action's display name — changed to the public identity **Owen**.
+
+## Why
+
+The project is language-neutral at the OwnIR/core level (`ownlang/` takes
+facts from any frontend that can produce them) and may eventually ship a
+TypeScript frontend alongside the current C# one. Publishing the first real
+package under a C#-specific name (`OwnSharp.Cli`, command `ownsharp`) would
+have locked the public identity to a single-language framing the project
+doesn't actually have. **Owen** is the product name; **this distribution
+currently includes the .NET/C# frontend only** — that framing is stated
+explicitly in the CLI's own `--help` output rather than left implicit.
+
+## Public identity implemented
+
+| Surface | Old | New |
+|---|---|---|
+| Product | (unnamed / "Own.NET" informally) | **Owen** |
+| NuGet package ID | `OwnSharp.Cli` | **`Owen.Cli`** (confirmed unclaimed on nuget.org at time of writing — both `owen` and `owen.cli` returned 404 from the v3 flat-container API; still worth a final check immediately before an actual publish, since availability can change) |
+| Tool command | `ownsharp` | **`owen`** |
+| Action display name | `Own.NET resource-leak check` | **`Owen lifetime/resource check`** |
+| SARIF `tool.driver.name` | `Own.NET` | **`Owen`** |
+| Cache directory | `~/.ownsharp/core//` | **`~/.owen/core//`** |
+| Preferred Python env var | `OWN_PYTHON` | **`OWEN_PYTHON`** (`OWN_PYTHON` still works, deprecated) |
+| Default Action SARIF filename | `own-net.sarif` | **`owen.sarif`** |
+
+## What deliberately did NOT change (internal names)
+
+Per the guardrail this rebrand was scoped to: no mass rename.
+
+- The `OwnSharp.Cli` **project and namespace** — still `OwnSharp.Cli` in the
+ `.csproj`, C# namespace, and `.sln`. Only `PackageId` and
+ `ToolCommandName` (the two properties that actually control the public
+ package/command identity) changed.
+- `AssemblyName` in `OwnSharp.Cli.csproj` stays `ownsharp` — it names the
+ internal `.dll` the `owen` shim launches, never typed or seen by a user,
+ so renaming it would add no user value (the csproj comment says so
+ explicitly, next to `ToolCommandName`).
+- **`OwnSharp.Extractor`** (project, namespace, and its real output filename
+ `ownsharp-extract.dll`) — completely untouched. `CheckCommand.cs`
+ references that literal filename because it is the actual file that ships,
+ not a stale pre-rebrand reference.
+- **`ownlang`** — the Python package name, its module names, its CLI
+ (`python -m ownlang ...`), and its `PYTHONPATH`/working-directory
+ conventions are all unchanged. Only the `"name"` string value inside the
+ SARIF `tool.driver` object (in `ownlang/ownir.py` and
+ `ownlang/diag_sarif.py`) changed from the literal `"Own.NET"` to `"Owen"`
+ — a metadata string, not a rename of anything importable.
+- **`OwnIR`**, **`OWN001`** and every other diagnostic code, the Rust crates
+ under `rust/`, every `frontend/*` directory name, and all historical
+ docs/issues — untouched. This note does not retroactively edit history;
+ older notes that say "Own.NET" or "ownsharp" describe what was true when
+ they were written.
+- `scripts/own-check.sh`/`.ps1` and `action.yml`'s internal call to
+ `scripts/own-check.sh` — unchanged. The Action's public *display name* and
+ *default SARIF filename* changed; what it runs under the hood did not.
+- The GitHub repository itself (`PhysShell/Own.NET`) — not renamed in this
+ PR. `_SARIF_INFO_URI` in `ownlang/ownir.py`/`diag_sarif.py` still points at
+ `https://github.com/PhysShell/Own.NET`, which remains accurate.
+- `audit/` and `scripts/{oracle_compare,mine_report}.py` still construct
+ synthetic test fixtures with a literal `"Own.NET"` driver name in a few
+ places. These are **test input fixtures** for those tools' own aggregation/
+ comparison logic (arbitrary strings a fixture author chose), not assertions
+ about Owen's real emitted SARIF — `audit/` is explicitly documented
+ (`audit/README.md`, `AGENTS.md`) as decoupled from `ownlang` and consuming
+ `own-check` only through its CLI/SARIF surface, with active development
+ living in a separate repo. Left alone to avoid scope creep into a module
+ this PR has no reason to touch.
+
+## CLI contract additions
+
+- **`owen check `** is the public invocation. `--help` is
+ language-neutral at the product level ("Owen finds lifetime and
+ resource-contract bugs") while explicitly listing what this distribution
+ actually wires up today:
+ ```text
+ Included frontend:
+ .NET / C# (.cs, .csproj, .sln)
+ ```
+ No plugin framework and no speculative TypeScript mention were added —
+ the help text does not claim support that doesn't exist yet.
+- **Unsupported input fails explicitly.** Before this rebrand, pointing
+ `check` at a `.ts` file or an empty/non-C# directory silently printed
+ "0 findings" — indistinguishable from a genuinely clean C# scan. The
+ explicit-failure behavior now has two layers, split across the two
+ components that each know a different half of the answer (review, PR
+ #246 round 2): `CheckCommand.HasSupportedInput` does only the CHEAP,
+ obvious check — an existing file with a recognized extension, or an
+ existing directory — *before* running the extractor at all, catching a
+ bare `.ts` file or a nonexistent path for free. `SupportedExtensions` is
+ compared with `StringComparer.OrdinalIgnoreCase` (review, PR #246 round
+ 2: `Foo.CS`/`App.CSPROJ` are the same file kind as their lowercase
+ spellings, on Windows/macOS filesystems and to MSBuild itself). The CLI
+ does **not** also try to duplicate the extractor's directory-walk skip
+ rules to predict whether a directory will actually yield anything — a
+ directory containing only `bin/`, `obj/`, or generated (`.g.cs`,
+ `.Designer.cs`, `.AssemblyInfo.cs`) files passed the CLI's cheap check
+ fine but produced zero real extractor inputs, recreating the exact
+ silent-clean-scan bug this exit tier exists to prevent. Duplicating the
+ extractor's skip-list in the CLI would just drift from it over time
+ (review, PR #246 round 2 explicit instruction: "do not duplicate the
+ complete extractor expansion rules in the CLI"). Instead, the extractor
+ itself is now the sole authority on "found nothing after expansion":
+ `OwnSharp.Extractor/Program.cs` checks `Expand(rawInputs).Distinct()`
+ immediately after computing it and returns a new exit code **4** with an
+ explicit message if that list is empty — a `.csproj`/`.sln` with no
+ usable source resolves the same way, since project/solution resolution
+ is text/glob-based, not full MSBuild evaluation (see the `ProjectCsFiles`
+ comment). `CheckCommand.RunAsync`'s existing `extractRc != 0` early
+ return propagates that 4 unchanged, and `own-check.sh`'s existing `set
+ -e` does the same for the script/Action path — no extra code needed on
+ either side beyond the extractor's own check. This is a new, additive
+ exit-code tier; it does not change any of the existing 0/1/`>=2`/3
+ contract.
+- **`OWEN_PYTHON`** is the preferred env var; the legacy **`OWN_PYTHON`**
+ name is still accepted as a temporary compatibility fallback (so existing
+ internal use — CI, scripts, muscle memory — doesn't break outright) and
+ prints a one-line deprecation note to stderr every time it's the variable
+ actually used to resolve Python. `OWEN_PYTHON` takes priority when both
+ are set.
+- **Cache directory** moved to `~/.owen/core///`,
+ where `fingerprint` is a SHA-256 over every vendored file's name and
+ content (length-prefixed encoding, so e.g. name `"ab"` + content `"c"`
+ can't collide with name `"a"` + content `"bc"` by bare concatenation).
+ **Correction (review, PR #246 round 3):** the round-2 design (a
+ version-keyed directory with a separate marker file holding a source
+ fingerprint) had a gap the reviewer's reproduction demonstrated directly:
+ on a fingerprint mismatch, the fix copied new files over the existing
+ destination with `overwrite: true`, then rewrote the marker to match the
+ new source — but a file the new source no longer has (e.g. a module
+ deleted upstream) was never removed from the destination, so it survived
+ as an orphan while the rewritten marker now claimed the (polluted)
+ directory was valid. Round 3 removes the marker concept entirely and
+ makes cache identity content-addressed: the fingerprint *is* the last
+ path segment, so a content change is a different path, never an
+ in-place overwrite of an existing one. Publication into a fresh path is
+ atomic — build into a `.tmp-` sibling directory, recompute the
+ fingerprint of what was actually written there, confirm it equals the
+ source fingerprint, and only then `Directory.Move` it to the final
+ fingerprint-named path — so a reader can only ever observe nothing, or a
+ fully-written self-verified copy, never a partial one (a crash or a lost
+ race with a concurrent `owen` process mid-copy just leaves a harmless
+ orphaned temp directory that nothing consults). The previous flat
+ `~/.ownsharp/core//` location (pre-rebrand layout, no
+ fingerprint segment) is still checked first and used in place without
+ copying — but only after fingerprinting what is *actually on disk* there
+ right now and confirming it equals the current source fingerprint; a
+ destination with extra, missing, or modified files fails that check and
+ falls through to a fresh unpack instead of being trusted or patched.
+ This is still a plain fallback *read*: the legacy location is never
+ written to, moved, or deleted by this code — content-addressing didn't
+ change that guardrail, only how "is this destination actually still
+ correct" gets decided.
+ **Correction (review, PR #246 round 4):** round 3 still trusted a hit at
+ the CURRENT fingerprint-named path (`Directory.Exists(finalOwnlang)`) on
+ existence alone — the concurrent-publisher race checks did too. But a
+ path name is only ever a claim; nothing stopped a destination already
+ living at the "right" fingerprint from being modified, corrupted, or
+ hand-assembled after the fact, and that content would then be served as
+ if it were still the exact bytes the fingerprint names. Every current-
+ path hit (the initial check, and both concurrent-publisher checks around
+ the atomic move) now recomputes the fingerprint over what is actually on
+ disk there and only trusts it on an exact match, exactly like the legacy
+ fallback already did. A mismatch quarantines the invalid destination —
+ an atomic rename to a `.invalid-` sibling (so no concurrent reader
+ ever observes an in-place delete mid-way), then a best-effort recursive
+ delete of the renamed copy — and falls through to the same temp-
+ directory-plus-atomic-move rebuild a fresh unpack uses; since the source
+ didn't change, the rebuild lands back at the identical fingerprint-named
+ path, now holding a verified copy.
+
+## Tests
+
+`ci.yml`'s `ownsharp-cli-smoke` job (job key kept as-is; only its *content*
+changed — renaming the key isn't part of the public facade and would just
+add unrelated churn) now pins, on both `ubuntu-latest` and `windows-latest`:
+package ID (`Owen.Cli`), command name (`owen`), `--help` (Owen framing +
+explicit included-frontend list + no TypeScript claim), `--version`,
+unknown-command prefix (`owen:`), a leak sample and a clean negative control
+both run from an installed, checkout-free location, the `Owen` SARIF driver
+name, the exit-4 unsupported-input path (explicitly asserting the output
+does **not** contain the literal clean-scan phrase `0 findings.`),
+`OWEN_PYTHON`'s not-found path, and the legacy `OWN_PYTHON` fallback
+(resolved successfully + its deprecation note). Two other pre-existing
+`ci.yml` assertions that pinned the literal SARIF driver-name string
+(`own-check-codescan`'s local structural-validation job, and
+`tests/test_diag_sarif.py`/`tests/test_ownir.py`) were updated from
+`"Own.NET"` to `"Owen"` to match — these are follow-on fixes made necessary
+by the driver-name change, not scope creep; they were caught by
+`python tests/run_tests.py` failing before the fix.
+
+**Added in review round 3** — the reviewer's stated concern was that both
+bugs (stale-cache pollution, cheap-preflight/extractor disagreement) were
+"sufficiently non-obvious that a future cleanup could reintroduce [them]
+while the ordinary fresh-install smoke remains green," so both
+reproductions from the review became committed `ci.yml` steps rather than
+staying local manual verification. All of the below ran locally first
+(pack -> isolated-feed install -> exercise -> confirm exit code/output)
+before being encoded as assertions:
+- **Matching legacy cache is reused in place** — the legitimate case: a
+ byte-for-byte-identical legacy `~/.ownsharp/...` unpack is used without
+ copying (`~/.owen` is asserted to *not* get created).
+- **Extra (removed-upstream) file in the legacy cache is rejected** — the
+ exact reproduction from the review: an old cache holds a file the new
+ source no longer has. A fresh, clean `~/.owen` unpack must result, with
+ the stale file absent from it.
+- **Modified-content legacy cache (same filenames, different bytes) is
+ rejected** — same version, same file set, different content: the
+ same-version class of bug the fingerprint exists to catch generally, not
+ just the removed-file case.
+- **Directory containing only skipped files** (`bin/`/`obj/`) and
+ **directory containing only generated files** (`*.g.cs`) both assert
+ exit 4 with an explicit "no supported input" message — the CLI's cheap
+ preflight passes these (a directory that exists), and only the
+ extractor's own zero-expanded-input check catches them.
+- **Empty `.csproj`** (no `Compile` items resolve to any `.cs` file) and
+ **`.sln` with no `Project(` lines** both assert exit 4 the same way.
+- **Skipped files alongside one real source file** asserts exit 1 with
+ `OWN001` still found — proving the skip rules don't over-reject a
+ directory that has genuine content alongside the noise.
+- **An inaccessible subtree alongside one readable source file** (`chmod
+ 000` on a subdirectory, meaningful on the hosted runners' non-root
+ accounts unlike this session's root-based local sandbox) asserts no
+ crash and the readable source's finding is still reported — the
+ `EnumerationOptions { IgnoreInaccessible = true }` tolerance from review
+ round 2, now with a real permission-denied subdirectory to exercise it.
+- **Uppercase extension (`Leak.CS`)** asserts exit 1 with `OWN001` found —
+ the `StringComparer.OrdinalIgnoreCase` fix from review round 3.
+- **A tampered CURRENT (fingerprint-named) cache is rejected and rebuilt**
+ (review round 4) — runs once to create `~/.owen/core///`,
+ tampers `ownlang/ownir.py`'s bytes and adds an `ownlang/stale_module.py`
+ directly under that exact path (not the legacy location the earlier
+ tests already covered), then asserts the second run's SARIF driver is
+ still `Owen`, `stale_module.py` is gone from whatever cache directory
+ actually got used, and `ownir.py` no longer contains the tampered
+ content anywhere under `~/.owen`.
+
+## PR separation
+
+This is PR 1 of 3 for the release-readiness work:
+
+1. **This PR** — the public facade rebrand (Owen identity, no publish).
+2. Phase 3 (NuGet release/package pipeline) — rebuilt against this PR once
+ merged, targeting `Owen.Cli`/`owen` instead of the pre-rebrand
+ `OwnSharp.Cli`/`ownsharp` identity its first draft (PR #244) used.
+3. Phase 4 (GitHub Marketplace preparation) — rebuilt against this PR once
+ merged, targeting the `Owen lifetime/resource check` display name instead
+ of the pre-rebrand name its first draft (PR #245) used.
+
+No oracle remeasurement and no analyzer-semantics change are mixed into this
+PR — the extractor, the core's detection logic, and every diagnostic
+verdict are byte-for-byte unchanged; `python tests/run_tests.py`, `ruff`,
+and `mypy` all stay green throughout.
diff --git a/frontend/roslyn/OwnSharp.Cli/CheckCommand.cs b/frontend/roslyn/OwnSharp.Cli/CheckCommand.cs
index 6bcbbfe3..de6ab957 100644
--- a/frontend/roslyn/OwnSharp.Cli/CheckCommand.cs
+++ b/frontend/roslyn/OwnSharp.Cli/CheckCommand.cs
@@ -3,16 +3,35 @@
namespace OwnSharp.Cli;
///
-/// `ownsharp check` — extract (bundled Roslyn extractor, in a child process)
+/// `owen check` — extract (bundled Roslyn extractor, in a child process)
/// -> facts.json -> the vendored core (system Python) -> render. Flags mirror
/// scripts/own-check.sh 1:1; the exit-code contract is the same one (own-check
/// comment): 0 clean, 1 findings, >=2 a hard error, plus --fail-on-finding.
+/// Exit 4 means no analyzable input: either this preflight's cheap check
+/// rejects every path outright (), or every
+/// path existed but the EXTRACTOR's own expansion (which alone knows its
+/// skip rules — bin/obj, generated, vendor trees) found nothing after
+/// filtering, in which case the extractor itself returns 4 and this command
+/// simply propagates it (review, PR #246: the CLI must not duplicate the
+/// extractor's expansion/skip rules to guess that outcome itself).
///
internal static class CheckCommand
{
private static readonly HashSet ValidFormats = ["human", "github", "msbuild", "sarif"];
private static readonly HashSet ValidSeverities = ["error", "warning"];
+ // The product (Owen) is language-neutral at the OwnIR/core level; this
+ // distribution currently wires up only the .NET/C# frontend. Naming the
+ // extensions here (instead of e.g. "just try everything and see") is
+ // what makes the CHEAP half of "unsupported input fails explicitly"
+ // possible: an obviously-wrong bare file (.ts, no extension at all,
+ // nonexistent path) is rejected here without spinning up the extractor
+ // at all. Case-insensitive (review, PR #246): Windows/macOS filesystems
+ // commonly are, and MSBuild itself treats `Foo.CS`/`App.CSPROJ` as the
+ // same file kinds as their lowercase spellings.
+ private static readonly HashSet SupportedExtensions =
+ new(StringComparer.OrdinalIgnoreCase) { ".cs", ".csproj", ".sln" };
+
public static async Task RunAsync(string[] args)
{
string format;
@@ -36,13 +55,13 @@ public static async Task RunAsync(string[] args)
if (!ValidFormats.Contains(format))
{
Console.Error.WriteLine(
- $"ownsharp check: unknown --format '{format}' (choose: {string.Join(", ", ValidFormats)})");
+ $"owen check: unknown --format '{format}' (choose: {string.Join(", ", ValidFormats)})");
return 2;
}
if (!ValidSeverities.Contains(severity))
{
Console.Error.WriteLine(
- $"ownsharp check: unknown --severity '{severity}' (choose: {string.Join(", ", ValidSeverities)})");
+ $"owen check: unknown --severity '{severity}' (choose: {string.Join(", ", ValidSeverities)})");
return 2;
}
if (paths.Count == 0)
@@ -50,6 +69,15 @@ public static async Task RunAsync(string[] args)
paths.Add(".");
}
+ if (!HasSupportedInput(paths, out var reason))
+ {
+ Console.Error.WriteLine($"owen check: no supported input found — {reason}");
+ Console.Error.WriteLine(
+ "Included frontend: .NET / C# (.cs, .csproj, .sln). " +
+ "This is not a clean scan: nothing was analyzed.");
+ return 4;
+ }
+
// Resolve Python FIRST: no point extracting facts just to fail on stage 2.
ResolvedPython python;
try
@@ -134,23 +162,65 @@ private static string RequireValue(string[] args, ref int i, string flag)
{
if (i + 1 >= args.Length)
{
- throw new InvalidOperationException($"ownsharp check: {flag} requires a value");
+ throw new InvalidOperationException($"owen check: {flag} requires a value");
}
return args[++i];
}
+ /// True if at least one of is CHEAPLY,
+ /// OBVIOUSLY plausible input for the currently included frontend: an
+ /// existing file whose extension is in ,
+ /// or an existing directory. Deliberately NOT a full expansion — a
+ /// directory is accepted here even if every .cs file under it
+ /// turns out to be skipped (bin/obj, generated, vendor) once the
+ /// extractor actually walks it; duplicating that skip-list in the CLI
+ /// would drift from the extractor's real rules (review, PR #246). The
+ /// extractor itself is the sole authority on "found nothing after
+ /// expansion" and returns exit 4 for that case (Program.cs) — this
+ /// preflight only catches the cheaper "obviously not C# at all" case
+ /// (nonexistent path, or a bare file with the wrong extension) without
+ /// paying for an extractor invocation.
+ private static bool HasSupportedInput(IReadOnlyList paths, out string reason)
+ {
+ var problems = new List();
+ foreach (var p in paths)
+ {
+ if (Directory.Exists(p))
+ {
+ reason = "";
+ return true;
+ }
+ if (File.Exists(p))
+ {
+ if (SupportedExtensions.Contains(Path.GetExtension(p)))
+ {
+ reason = "";
+ return true;
+ }
+ problems.Add($"'{p}' has an unsupported extension ({Path.GetExtension(p)})");
+ continue;
+ }
+ problems.Add($"'{p}' does not exist");
+ }
+ reason = string.Join("; ", problems);
+ return false;
+ }
+
/// Stage 1: run the bundled extractor as a child process. All of its
/// own output (build/run chatter, if any) goes to OUR stderr, keeping
/// stdout clean for stage 2 — same as own-check.sh's `1>&2` on this stage.
private static async Task RunExtractorAsync(
IReadOnlyList paths, string factsPath, bool legacy, bool stats, bool bodyThrowEdges)
{
+ // "ownsharp-extract.dll" is OwnSharp.Extractor's own real AssemblyName/output
+ // filename (internal project name, unchanged by the Owen public facade) —
+ // this is the file that actually ships, not a stale reference.
var extractorDll = Path.Combine(AppContext.BaseDirectory, "ownsharp-extract.dll");
if (!File.Exists(extractorDll))
{
Console.Error.WriteLine(
- $"ownsharp: bundled extractor not found at '{extractorDll}' — a corrupt or " +
- "incomplete tool install. Try `dotnet tool uninstall --global OwnSharp.Cli` and reinstall.");
+ $"owen: bundled extractor not found at '{extractorDll}' — a corrupt or " +
+ "incomplete tool install. Try `dotnet tool uninstall --global Owen.Cli` and reinstall.");
return 2;
}
@@ -182,7 +252,7 @@ private static async Task RunExtractorAsync(
}
using var proc = Process.Start(psi)
- ?? throw new InvalidOperationException("ownsharp: failed to start the extractor process");
+ ?? throw new InvalidOperationException("owen: failed to start the extractor process");
var stdoutTask = proc.StandardOutput.ReadToEndAsync();
var stderrTask = proc.StandardError.ReadToEndAsync();
await proc.WaitForExitAsync().ConfigureAwait(false);
@@ -199,8 +269,8 @@ private static async Task RunExtractorAsync(
/// lookup is the reliable default; DOTNET_ROOT (set by some CI/sandboxed
/// installs) is honored first when present. Deliberately NOT
/// Process.GetCurrentProcess().MainModule — on Windows a `dotnet tool`
- /// shim is a native apphost, so that would resolve to ownsharp.exe itself,
- /// not the dotnet muxer.
+ /// shim is a native apphost, so that would resolve to owen.exe itself
+ /// (the ToolCommandName-based shim), not the dotnet muxer.
private static string ResolveDotnetMuxer()
{
var root = Environment.GetEnvironmentVariable("DOTNET_ROOT");
@@ -245,7 +315,7 @@ private static async Task RunCoreAsync(
psi.EnvironmentVariables["PYTHONPATH"] = cacheRoot;
using var proc = Process.Start(psi)
- ?? throw new InvalidOperationException("ownsharp: failed to start the Python core process");
+ ?? throw new InvalidOperationException("owen: failed to start the Python core process");
await proc.WaitForExitAsync().ConfigureAwait(false);
return proc.ExitCode;
}
diff --git a/frontend/roslyn/OwnSharp.Cli/CoreVendor.cs b/frontend/roslyn/OwnSharp.Cli/CoreVendor.cs
index 52b3a31f..29b16c9d 100644
--- a/frontend/roslyn/OwnSharp.Cli/CoreVendor.cs
+++ b/frontend/roslyn/OwnSharp.Cli/CoreVendor.cs
@@ -1,11 +1,52 @@
+using System.Security.Cryptography;
+using System.Text;
+
namespace OwnSharp.Cli;
///
/// Unpacks the vendored ownlang/ Python source (packed into this tool's
/// own nupkg under ownlang-core/ownlang/*.py, see the .csproj) into a
-/// stable, per-version cache directory outside the tool's own (versioned,
-/// nested) install path — and, per the design decision in issue #202, never
-/// into the repository being analyzed.
+/// stable, content-addressed cache directory outside the tool's own
+/// (versioned, nested) install path — and, per the design decision in issue
+/// #202, never into the repository being analyzed.
+///
+/// Layout: ~/.owen/core/<version>/<fingerprint>/ownlang/, where
+/// fingerprint is a SHA-256 over every vendored file's name and
+/// content (see ). Content-addressing (review, PR
+/// #246) closes a hole a plain version-keyed marker had: the CLI's own
+/// <Version> does not change every time the vendored core's
+/// content does — this rebrand's own SARIF-driver-name change is the
+/// concrete proof, same 0.1.0, different core content — so a
+/// version-only cache could either serve stale content on a mismatch, or
+/// (worse) get overwritten file-by-file in place, leaving files the new
+/// source no longer has stranded alongside the new ones with a marker that
+/// then claims the (polluted) result matches. Content-addressing sidesteps
+/// both: a fingerprint mismatch is a *different path*, never an overwrite of
+/// an existing one, and publication into that path is atomic (build in a
+/// temp sibling, then once
+/// fully written and verified) so a reader never observes a partial write.
+///
+/// A version already unpacked under the previous flat
+/// ~/.ownsharp/core/<version>/ location (pre-rebrand layout, no
+/// fingerprint) is used in place — without copying — but ONLY after
+/// recomputing a fingerprint over what is actually on disk there and
+/// confirming it matches the source this install bundles right now; a
+/// destination with extra, missing, or modified files fails that check and
+/// falls through to a fresh unpack. This is still a plain fallback *read*,
+/// not a migration subsystem: the legacy location is never written to,
+/// moved, or deleted by this code.
+///
+/// A hit at the CURRENT (fingerprint-named) path is verified the same way
+/// (review, PR #246 round 4) — the path's name is only ever a claim, not
+/// proof; something could have modified, corrupted, or hand-assembled a
+/// directory that happens to sit at the "right" fingerprint since it was
+/// published. is recomputed over what is actually
+/// there on every hit (both the initial existence check and the
+/// concurrent-publisher race checks further down) and only trusted on an
+/// exact match; a mismatch quarantines the invalid destination (an atomic
+/// rename out of the way, then best-effort delete — never an in-place
+/// delete a concurrent reader could observe mid-way) and falls through to
+/// the same temp-directory + atomic-move rebuild used for a fresh unpack.
///
internal static class CoreVendor
{
@@ -17,36 +58,178 @@ internal static class CoreVendor
///
public static string EnsureUnpacked()
{
- var cacheRoot = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".ownsharp", "core", ToolVersion.Current);
- var destOwnlang = Path.Combine(cacheRoot, "ownlang");
- var marker = Path.Combine(cacheRoot, ".unpacked");
-
- if (File.Exists(marker))
- {
- return cacheRoot;
- }
-
var sourceOwnlang = Path.Combine(AppContext.BaseDirectory, "ownlang-core", "ownlang");
if (!Directory.Exists(sourceOwnlang))
{
throw new InvalidOperationException(
- $"ownsharp: vendored core not found at '{sourceOwnlang}' — a corrupt or " +
- "incomplete tool install. Try `dotnet tool uninstall --global OwnSharp.Cli` " +
+ $"owen: vendored core not found at '{sourceOwnlang}' — a corrupt or " +
+ "incomplete tool install. Try `dotnet tool uninstall --global Owen.Cli` " +
"and reinstall.");
}
+ var sourceFiles = SortedPyFiles(sourceOwnlang);
+ var fingerprint = Fingerprint(sourceFiles);
+
+ var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ var versionRoot = Path.Combine(userProfile, ".owen", "core", ToolVersion.Current);
+ var finalRoot = Path.Combine(versionRoot, fingerprint);
+ var finalOwnlang = Path.Combine(finalRoot, "ownlang");
- Directory.CreateDirectory(destOwnlang);
- foreach (var file in Directory.EnumerateFiles(sourceOwnlang, "*.py"))
+ // Content-addressed cache hit: verify the DESTINATION's actual content,
+ // not just its existence at the fingerprint-named path (review, PR #246
+ // round 4) -- a directory living under the "right" path is not proof it
+ // still holds the exact bytes that path name claims; only recomputing
+ // the fingerprint over what is actually there is. A mismatch means this
+ // path is invalid -- content-addressing has no business trusting it (it
+ // is not a "different, still-valid" cache the way a different
+ // fingerprint would be) -- quarantine it and fall through to rebuild.
+ if (Directory.Exists(finalOwnlang))
+ {
+ if (DestinationMatches(finalOwnlang, fingerprint))
+ {
+ return finalRoot;
+ }
+ QuarantineInvalidDestination(finalRoot);
+ }
+
+ // Legacy fallback: verify the LEGACY DESTINATION's actual content, not a
+ // marker file's say-so (review, PR #246) -- a marker only proves "an
+ // unpack happened here once", never that nothing since removed, added,
+ // or modified a file in that directory.
+ var legacyOwnlang = Path.Combine(userProfile, ".ownsharp", "core", ToolVersion.Current, "ownlang");
+ if (Directory.Exists(legacyOwnlang) && DestinationMatches(legacyOwnlang, fingerprint))
+ {
+ return Path.Combine(userProfile, ".ownsharp", "core", ToolVersion.Current);
+ }
+
+ // Fresh unpack: build into a temp sibling, verify the DESTINATION's own
+ // fingerprint matches the source before anything else can observe it,
+ // then publish with a single atomic rename. A reader can only ever see
+ // either nothing at finalOwnlang, or a fully-written, self-verified copy
+ // -- never a partial one (crash/kill/full-disk mid-copy just leaves an
+ // orphaned temp directory next to it, harmless and never consulted).
+ var tempRoot = Path.Combine(versionRoot, $".tmp-{Guid.NewGuid():N}");
+ var tempOwnlang = Path.Combine(tempRoot, "ownlang");
+ try
+ {
+ Directory.CreateDirectory(tempOwnlang);
+ foreach (var file in sourceFiles)
+ {
+ File.Copy(file, Path.Combine(tempOwnlang, Path.GetFileName(file)), overwrite: true);
+ }
+ var writtenFiles = SortedPyFiles(tempOwnlang);
+ var writtenFingerprint = Fingerprint(writtenFiles);
+ if (writtenFingerprint != fingerprint)
+ {
+ throw new InvalidOperationException(
+ $"owen: internal error -- the core copy at '{tempOwnlang}' does not match " +
+ "its source fingerprint after writing. Not publishing it; try reinstalling.");
+ }
+
+ if (Directory.Exists(finalOwnlang))
+ {
+ // Possibly lost a race with a concurrent `owen` process that
+ // published the same fingerprint first -- but only trust that if
+ // ITS destination actually verifies (review, PR #246 round 4).
+ // Existence proves nothing about a path anyone (or anything)
+ // could have written to since; "same fingerprint-named path" is
+ // not the same claim as "same, provably identical content".
+ if (DestinationMatches(finalOwnlang, fingerprint))
+ {
+ return finalRoot;
+ }
+ QuarantineInvalidDestination(finalRoot);
+ }
+ try
+ {
+ Directory.CreateDirectory(finalRoot);
+ Directory.Move(tempOwnlang, finalOwnlang);
+ }
+ catch (IOException) when (Directory.Exists(finalOwnlang))
+ {
+ // Narrower version of the same race (review, PR #246): a concurrent
+ // process created finalOwnlang between the check above and this
+ // Move. Same verification requirement as above -- only accept it
+ // if it actually matches; otherwise quarantine it and retry the
+ // move once with our own already-verified tempOwnlang copy.
+ if (DestinationMatches(finalOwnlang, fingerprint))
+ {
+ return finalRoot;
+ }
+ QuarantineInvalidDestination(finalRoot);
+ Directory.CreateDirectory(finalRoot);
+ Directory.Move(tempOwnlang, finalOwnlang);
+ }
+ return finalRoot;
+ }
+ finally
+ {
+ if (Directory.Exists(tempRoot))
+ {
+ try { Directory.Delete(tempRoot, recursive: true); } catch (IOException) { /* best-effort cleanup */ }
+ }
+ }
+ }
+
+ private static List SortedPyFiles(string dir) =>
+ Directory.EnumerateFiles(dir, "*.py").OrderBy(f => Path.GetFileName(f), StringComparer.Ordinal).ToList();
+
+ /// True only if every .py file actually on disk under
+ /// right now fingerprints to
+ /// (review, PR #246 round 4). This
+ /// is the sole source of truth for "is this destination still valid" --
+ /// a directory's location (even a content-addressed, fingerprint-named
+ /// one) is only ever a claim about what was published there once, never
+ /// proof of what is there now.
+ private static bool DestinationMatches(string ownlangDir, string expectedFingerprint) =>
+ Fingerprint(SortedPyFiles(ownlangDir)) == expectedFingerprint;
+
+ /// Moves an invalid cache destination out of the way of a rebuild
+ /// (review, PR #246 round 4). Renames first -- an atomic same-volume
+ /// rename can't be observed half-done the way an in-place recursive
+ /// delete could -- then best-effort deletes the renamed copy; a failure
+ /// there just leaves inert garbage that is never consulted again (the
+ /// quarantined name is never re-derived by ),
+ /// same reasoning as the orphaned-temp-directory cleanup above.
+ private static void QuarantineInvalidDestination(string invalidRoot)
+ {
+ var quarantined = $"{invalidRoot}.invalid-{Guid.NewGuid():N}";
+ try
+ {
+ Directory.Move(invalidRoot, quarantined);
+ }
+ catch (IOException)
+ {
+ // Lost a race with something else already handling this exact path
+ // (e.g. a concurrent process's own quarantine of the same invalid
+ // directory) -- nothing more to do; the caller re-checks fresh.
+ return;
+ }
+ try { Directory.Delete(quarantined, recursive: true); } catch (IOException) { /* best-effort cleanup */ }
+ }
+
+ /// SHA-256 over every file's name and content, each explicitly
+ /// length-prefixed (review, PR #246) so two different (name, content) sets
+ /// can never hash identically by having their bytes merely concatenate the
+ /// same way -- e.g. name "ab" + content "c" vs. name "a" + content "bc"
+ /// would collide under bare concatenation; an 8-byte length prefix on each
+ /// field rules that out. Sorted by filename first (by the caller) for a
+ /// fingerprint that doesn't depend on enumeration order.
+ private static string Fingerprint(IReadOnlyList files)
+ {
+ using var sha = SHA256.Create();
+ using var buffer = new MemoryStream();
+ using var writer = new BinaryWriter(buffer, Encoding.UTF8, leaveOpen: true);
+ foreach (var file in files)
{
- var dest = Path.Combine(destOwnlang, Path.GetFileName(file));
- File.Copy(file, dest, overwrite: true);
+ var nameBytes = Encoding.UTF8.GetBytes(Path.GetFileName(file));
+ writer.Write((long)nameBytes.Length);
+ writer.Write(nameBytes);
+ var contentBytes = File.ReadAllBytes(file);
+ writer.Write((long)contentBytes.Length);
+ writer.Write(contentBytes);
}
- // Write the marker LAST: an interrupted copy (killed process, full disk)
- // leaves no marker, so the next run redoes the unpack instead of running
- // against a half-written core.
- File.WriteAllText(marker, ToolVersion.Current);
- return cacheRoot;
+ writer.Flush();
+ buffer.Position = 0;
+ return Convert.ToHexString(sha.ComputeHash(buffer));
}
}
diff --git a/frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj b/frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj
index da73dbbe..92880756 100644
--- a/frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj
+++ b/frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj
@@ -12,6 +12,14 @@
pieces (extractor -> core) the same way scripts/own-check.sh already
does, so they ship as ONE `dotnet tool install`.
+ Public facade (docs/notes/owen-public-facade.md): the PROJECT stays
+ "OwnSharp.Cli" internally (not mass-renamed — that would be an
+ internal refactor, not what this is), but the PUBLISHED package ID,
+ command name, and all user-facing output are "Owen"/"owen". Product
+ "Owen" is language-neutral at the OwnIR/core level; this NuGet
+ package is specifically the .NET/C# frontend distribution of it —
+ see Program.cs's help text for the exact honest framing.
+
Packaging shape (design decision recorded in issue #202):
- The Roslyn extractor (OwnSharp.Extractor, unmodified, P-013) is
pulled in via ProjectReference below; its build output (dll +
@@ -23,16 +31,22 @@
a bundled binary instead of running the extractor from source.
- The Python core (ownlang/, zero-dependency pure Python, see
../../../pyproject.toml) is vendored as loose *.py content below
- and unpacked to ~/.ownsharp/core// on first run; it is
- executed by the machine's own Python (>=3.11), never embedded or
- compiled. See Program.cs for the resolution/unpack/fail-fast
- logic and the rejected alternatives on record in issue #202.
+ and unpacked to ~/.owen/core// on first run (falling back
+ to a previous ~/.ownsharp/core// if already unpacked
+ there — see CoreVendor.cs); it is executed by the machine's own
+ Python (>=3.11), never embedded or compiled. See Program.cs for
+ the resolution/unpack/fail-fast logic and the rejected
+ alternatives on record in issue #202.
-->
true
- ownsharp
- OwnSharp.Cli
+
+ owen
+ Owen.Cli
0.1.0
- Own.NET's single command: `ownsharp check <path|.sln>` wraps the Roslyn extractor and the Python core (run on system Python) into one dotnet tool install.
+ Owen finds lifetime and resource-contract bugs. This distribution currently includes the .NET/C# frontend: `owen check <path|.sln>` wraps the Roslyn extractor and the Python core (run on system Python) into one dotnet tool install.
diff --git a/frontend/roslyn/OwnSharp.Cli/Program.cs b/frontend/roslyn/OwnSharp.Cli/Program.cs
index 56d7a76f..75b7fa4b 100644
--- a/frontend/roslyn/OwnSharp.Cli/Program.cs
+++ b/frontend/roslyn/OwnSharp.Cli/Program.cs
@@ -1,6 +1,9 @@
-// ownsharp — the single command for alpha gate A (issue #202).
+// owen — the single command for alpha gate A (issue #202), public facade
+// "Owen" (see docs/notes/owen-public-facade.md). The underlying project/
+// namespace stays OwnSharp.Cli internally -- this is a public-facing rename,
+// not an internal refactor.
//
-// `ownsharp check ` wraps the two existing pipeline stages —
+// `owen check ` wraps the two existing pipeline stages —
// extractor -> core — into one `dotnet tool install`. This file only does
// verb dispatch; the real work is in CheckCommand.cs / PythonResolver.cs /
// CoreVendor.cs. No analysis logic lives here or anywhere in this project:
@@ -22,20 +25,28 @@
if (args[0] != "check")
{
- Console.Error.WriteLine($"ownsharp: unknown command '{args[0]}'");
+ Console.Error.WriteLine($"owen: unknown command '{args[0]}'");
Console.Error.WriteLine(HelpText());
return 2;
}
return await CheckCommand.RunAsync(args[1..]).ConfigureAwait(false);
+// Product framing is deliberately language-neutral (Owen finds lifetime and
+// resource-contract bugs; the OwnIR/core layer is not C#-specific) while the
+// "Included frontend" line is explicit about what THIS distribution actually
+// wires up today -- no plugin framework, no speculative TypeScript claim.
static string HelpText() => """
- ownsharp — find lifetime/resource bugs in C# (Own.NET)
+ owen — finds lifetime and resource-contract bugs.
+ This distribution currently includes the .NET/C# frontend.
+
+ Included frontend:
+ .NET / C# (.cs, .csproj, .sln)
Usage:
- ownsharp check [more paths...] [options]
- ownsharp --version
- ownsharp --help
+ owen check [more paths...] [options]
+ owen --version
+ owen --help
Options (mirrors scripts/own-check.sh):
--format {human|github|msbuild|sarif} finding surface (default: human)
@@ -46,7 +57,11 @@ ownsharp check [more paths...] [options]
--stats print flow-locals coverage to stderr
--body-throw-edges opt-in: flag body-level (no-try) dispose-not-called-on-throw
- Python: resolved via OWN_PYTHON, else `py -3` (Windows) / `python3`
- (elsewhere); must be >=3.11. No auto-install — see the error message if
- none is found.
+ Python: resolved via OWEN_PYTHON (OWN_PYTHON is a deprecated, temporary
+ fallback), else `py -3` (Windows) / `python3` (elsewhere); must be
+ >=3.11. No auto-install — see the error message if none is found.
+
+ Input that doesn't match the included frontend (e.g. no .cs/.csproj/.sln
+ found anywhere given) fails explicitly (exit 4) rather than reporting a
+ clean scan.
""";
diff --git a/frontend/roslyn/OwnSharp.Cli/PythonResolver.cs b/frontend/roslyn/OwnSharp.Cli/PythonResolver.cs
index a59cb9cf..d78c5d2a 100644
--- a/frontend/roslyn/OwnSharp.Cli/PythonResolver.cs
+++ b/frontend/roslyn/OwnSharp.Cli/PythonResolver.cs
@@ -13,9 +13,13 @@ internal sealed class PythonNotFoundException(string message) : Exception(messag
internal sealed record ResolvedPython(string FileName, IReadOnlyList LeadingArgs);
///
-/// Resolution order (design decision, issue #202): OWN_PYTHON env var
-/// (used exactly as given, no fallback if it doesn't work — an explicit
-/// override that fails is a configuration error, not a "keep guessing" case),
+/// Resolution order (design decision, issue #202; env var renamed for the
+/// Owen public facade, see docs/notes/owen-public-facade.md):
+/// OWEN_PYTHON env var (used exactly as given, no fallback if it
+/// doesn't work — an explicit override that fails is a configuration error,
+/// not a "keep guessing" case); else the legacy OWN_PYTHON name (a
+/// temporary compatibility fallback so existing internal use doesn't break —
+/// prints a deprecation note to stderr whenever it's the one actually used);
/// else the platform default (py -3 on Windows, python3
/// elsewhere). No auto-download, ever: a miss is a fast, actionable failure.
///
@@ -26,19 +30,39 @@ internal static class PythonResolver
public static ResolvedPython Resolve()
{
- var ownPython = Environment.GetEnvironmentVariable("OWN_PYTHON");
- if (!string.IsNullOrWhiteSpace(ownPython))
+ var owenPython = Environment.GetEnvironmentVariable("OWEN_PYTHON");
+ if (!string.IsNullOrWhiteSpace(owenPython))
{
- var candidate = new ResolvedPython(ownPython, Array.Empty());
+ var candidate = new ResolvedPython(owenPython, Array.Empty());
if (TryGetVersion(candidate, out var version) && IsSupported(version))
{
return candidate;
}
throw new PythonNotFoundException(
- $"ownsharp: OWN_PYTHON='{ownPython}' did not resolve to Python >={MinMajor}.{MinMinor} " +
+ $"owen: OWEN_PYTHON='{owenPython}' did not resolve to Python >={MinMajor}.{MinMinor} " +
$"(found: {version ?? "not runnable"}). {InstallHint()}");
}
+ // Legacy fallback (temporary): OWN_PYTHON predates the Owen public
+ // facade and some existing/internal use still sets it. Honored so
+ // that doesn't silently break, but flagged every time it's the
+ // variable actually used, so it doesn't quietly become permanent.
+ var legacyOwnPython = Environment.GetEnvironmentVariable("OWN_PYTHON");
+ if (!string.IsNullOrWhiteSpace(legacyOwnPython))
+ {
+ var candidate = new ResolvedPython(legacyOwnPython, Array.Empty());
+ if (TryGetVersion(candidate, out var version) && IsSupported(version))
+ {
+ Console.Error.WriteLine(
+ "owen: OWN_PYTHON is deprecated — set OWEN_PYTHON instead (OWN_PYTHON is a " +
+ "temporary compatibility fallback and may be removed in a future release).");
+ return candidate;
+ }
+ throw new PythonNotFoundException(
+ $"owen: OWN_PYTHON='{legacyOwnPython}' did not resolve to Python >={MinMajor}.{MinMinor} " +
+ $"(found: {version ?? "not runnable"}). {InstallHint()} (OWN_PYTHON is deprecated — use OWEN_PYTHON.)");
+ }
+
var defaultCandidate = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? new ResolvedPython("py", ["-3"])
: new ResolvedPython("python3", Array.Empty());
@@ -64,8 +88,8 @@ public static ResolvedPython Resolve()
}
throw new PythonNotFoundException(
- $"ownsharp: no Python >={MinMajor}.{MinMinor} found on PATH. {InstallHint()} " +
- "(or set OWN_PYTHON to an interpreter's path).");
+ $"owen: no Python >={MinMajor}.{MinMinor} found on PATH. {InstallHint()} " +
+ "(or set OWEN_PYTHON to an interpreter's path).");
}
private static bool IsSupported(string? version)
diff --git a/frontend/roslyn/OwnSharp.Cli/README.md b/frontend/roslyn/OwnSharp.Cli/README.md
index 7c1deaf2..2a898297 100644
--- a/frontend/roslyn/OwnSharp.Cli/README.md
+++ b/frontend/roslyn/OwnSharp.Cli/README.md
@@ -1,8 +1,16 @@
-# ownsharp — the single command (alpha gate A, issue #202)
-
-`ownsharp check ` wraps the two existing pipeline stages —
-the Roslyn extractor (`OwnSharp.Extractor`, P-013) and the Python core
-(`ownlang/`) — into **one `dotnet tool install`**. Same pipeline
+# owen — the single command (alpha gate A, issue #202)
+
+Public facade: **Owen**, package **Owen.Cli**, command **`owen`**. The
+project/namespace stay `OwnSharp.Cli` internally — this is a public-facing
+rename, not an internal refactor; see
+[`docs/notes/owen-public-facade.md`](../../../docs/notes/owen-public-facade.md)
+for the full rationale and what did/didn't change.
+
+Owen finds lifetime and resource-contract bugs. It is language-neutral at the
+OwnIR/core level; **this distribution currently includes the .NET/C#
+frontend only** — `owen check ` wraps the two existing
+pipeline stages — the Roslyn extractor (`OwnSharp.Extractor`, P-013) and the
+Python core (`ownlang/`) — into **one `dotnet tool install`**. Same pipeline
[`scripts/own-check.sh`](../../../scripts/own-check.sh) already chains by
hand; this is that, packaged.
@@ -16,17 +24,24 @@ hand; this is that, packaged.
build output (dll + `.deps.json`/`.runtimeconfig.json` + Roslyn
dependencies) rides along in this tool's own pack payload because
`PackAsTool` packs the full publish closure. `check` invokes it as a child
- process (`dotnet exec /ownsharp-extract.dll ...`).
+ process (`dotnet exec /ownsharp-extract.dll ...` — the extractor's
+ own internal filename, unaffected by the public facade).
- **The core is unmodified**, vendored as loose `*.py` content (see the
- `.csproj`) and unpacked to `~/.ownsharp/core//` on first run —
- never into the analyzed repo. It runs on the machine's own Python; nothing
- is embedded, compiled, or downloaded.
-- **Python resolution**: `OWN_PYTHON` env var (used exactly as given, no
+ `.csproj`) and unpacked to `~/.owen/core//` on first run (falling
+ back to a previous `~/.ownsharp/core//` if already unpacked there
+ by an older install — a plain reuse, not a migration) — never into the
+ analyzed repo. It runs on the machine's own Python; nothing is embedded,
+ compiled, or downloaded.
+- **Python resolution**: `OWEN_PYTHON` env var (used exactly as given, no
fallback — an explicit override that fails is a config error, not a
- "keep guessing" case), else `py -3` (Windows) / `python3` (elsewhere),
- version-checked to be `>=3.11`. No Python found → a fast, one-line,
- actionable failure (`winget`/`apt`/`brew`/python.org, per OS) — **never**
- an auto-download.
+ "keep guessing" case); `OWN_PYTHON` is honored as a temporary, deprecated
+ fallback (prints a note to stderr when it's the one actually used); else
+ `py -3` (Windows) / `python3` (elsewhere), version-checked to be `>=3.11`.
+ No Python found → a fast, one-line, actionable failure
+ (`winget`/`apt`/`brew`/python.org, per OS) — **never** an auto-download.
+- **Unsupported input fails explicitly**: a path that isn't a `.cs`/`.csproj`/
+ `.sln` file and isn't a directory containing any `.cs` file exits 4 with an
+ explicit message — never a silent "0 findings" clean scan.
- **Rejected alternatives** (embedding a CPython runtime, self-contained
PyInstaller binaries as the default, waiting for the Rust core, porting the
core to C#) are on the record in the issue; do not re-litigate them here.
@@ -37,15 +52,15 @@ Not published to nuget.org yet (P-013's Non-goals) — build and install from
source:
```bash
-dotnet pack frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj -c Release -o /tmp/ownsharp-nupkg
-dotnet tool install --global OwnSharp.Cli --version 0.1.0 --add-source /tmp/ownsharp-nupkg
+dotnet pack frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj -c Release -o /tmp/owen-nupkg
+dotnet tool install --global Owen.Cli --version 0.1.0 --add-source /tmp/owen-nupkg
-ownsharp check MyApp.sln # human output
-ownsharp check . --format github --fail-on-finding # PR annotations, non-zero on a leak
-ownsharp check . --format sarif > own.sarif # feed github/codeql-action/upload-sarif
+owen check MyApp.sln # human output
+owen check . --format github --fail-on-finding # PR annotations, non-zero on a leak
+owen check . --format sarif > owen.sarif # feed github/codeql-action/upload-sarif
```
-Uninstall/upgrade: `dotnet tool uninstall --global OwnSharp.Cli`, then reinstall
+Uninstall/upgrade: `dotnet tool uninstall --global Owen.Cli`, then reinstall
as above (bump `--version` if you rebuilt with a new ``).
## Flags (mirror `scripts/own-check.sh` 1:1)
@@ -60,29 +75,22 @@ as above (bump `--version` if you rebuilt with a new ``).
| `--stats` | off | print flow-locals coverage to stderr |
| `--body-throw-edges` | off | opt-in: flag body-level (no-`try`) dispose-not-called-on-throw |
-Exit codes (same contract as `own-check.sh`/`.ps1`): the extractor stage's own
-exit code propagates on a hard failure there; otherwise `0` clean / `1`
-findings (only surfaced when `--fail-on-finding`) / `>=2` a core hard error
-(bad facts, a drifted contract) always propagates; `3` is `ownsharp`'s own —
-no usable Python was found.
-
-## Guardrails this project honors (no behaviour change, packaging only)
-
-- **No changes to `OwnSharp.Extractor`** — it is referenced, not edited.
-- **No changes to `ownlang/`** — vendored byte-identical; "one checker" holds
- literally, since the exact same core source renders every verdict.
-- **`scripts/own-check.sh`/`.ps1` and `action.yml` are untouched** and keep
- working exactly as before — this tool is a third surface alongside them, not
- a replacement (P-013 §Scope).
+Exit codes: `0` clean, `1` findings (only with `--fail-on-finding`), `>=2` a
+core hard error (bad facts, a drifted contract), `3` no usable Python found,
+`4` no supported input found (nothing matching the included frontend).
## CI proof
`ownsharp-cli-smoke` in `.github/workflows/ci.yml` (matrix: `ubuntu-latest` +
`windows-latest`) proves, on a clean runner: pack → `dotnet tool install
---global` → `ownsharp check` finds a real leak (`--fail-on-finding` exits 1,
-`OWN001` in the output) → the timed install-to-findings window stays under a
-regression ceiling → the no-Python path fails fast with the actionable
-message. Both platforms matter here specifically, not just "more coverage": a
-`dotnet tool` shim is a native apphost on Windows and a shell script on Unix —
-genuinely different process-launch mechanics, so ubuntu-only would not have
-proven the Windows path.
+--global` → `owen --help`/`--version`/unknown-command → `owen check` finds a
+real leak (`--fail-on-finding` exits 1, `OWN001` in the output) and stays
+silent on clean code (exit 0) → the SARIF surface carries the `Owen` driver
+name → unsupported input fails explicitly (exit 4, never a clean scan) →
+`OWEN_PYTHON` (and the deprecated `OWN_PYTHON` fallback, with its
+deprecation note) both resolve Python correctly → the no-Python path fails
+fast with an actionable message → the timed install-to-findings window stays
+under a regression ceiling. Both platforms matter here specifically, not
+just "more coverage": a `dotnet tool` shim is a native apphost on Windows and
+a shell script on Unix — genuinely different process-launch mechanics, so
+ubuntu-only would not have proven the Windows path.
diff --git a/frontend/roslyn/OwnSharp.Cli/ToolVersion.cs b/frontend/roslyn/OwnSharp.Cli/ToolVersion.cs
index b6a593b4..07cb2750 100644
--- a/frontend/roslyn/OwnSharp.Cli/ToolVersion.cs
+++ b/frontend/roslyn/OwnSharp.Cli/ToolVersion.cs
@@ -3,9 +3,11 @@
namespace OwnSharp.Cli;
///
-/// The running tool's own version — doubles as the vendored-core cache key
-/// (~/.ownsharp/core/<version>/), so a core mismatch between two
-/// installed tool versions can never share a cache directory.
+/// The running tool's own version — the first path segment of the
+/// vendored-core cache key (~/.owen/core/<version>/<fingerprint>/,
+/// see ), so a core mismatch between two installed
+/// tool versions can never share a cache directory even before the
+/// fingerprint segment is considered.
///
internal static class ToolVersion
{
diff --git a/frontend/roslyn/OwnSharp.Extractor/Program.cs b/frontend/roslyn/OwnSharp.Extractor/Program.cs
index 00a142e0..d3d57d69 100644
--- a/frontend/roslyn/OwnSharp.Extractor/Program.cs
+++ b/frontend/roslyn/OwnSharp.Extractor/Program.cs
@@ -462,6 +462,24 @@ static string Rel(string path) =>
var inputs = Expand(rawInputs).Distinct().ToList();
+// Every raw input resolved to nothing analyzable — every path either doesn't
+// exist, or every .cs file under it was filtered out by IsSkipped (bin/obj,
+// generated, vendor trees; review, PR #246). Distinct from the "no inputs
+// GIVEN at all" usage error above (exit 2): this is "inputs were given, but
+// none of them contain anything this frontend can read" — the same shape a
+// caller (owen check, own-check.sh) must not read as "0 findings, clean".
+// Exit 4 deliberately matches the CLI's own no-supported-input contract
+// (CheckCommand.cs) so a caller never needs to special-case which layer
+// caught it; own-check.sh/action.yml propagate any non-zero extractor exit
+// as-is (no python-core stage 2 is reached), same as the existing exit-2 path.
+if (inputs.Count == 0)
+{
+ Console.Error.WriteLine(
+ "extractor: no supported input found — every given path either does not exist " +
+ "or contains no .cs file after skipping bin/obj/generated/vendor trees.");
+ return 4;
+}
+
static bool IsHandler(ExpressionSyntax rhs) =>
rhs is IdentifierNameSyntax || rhs is MemberAccessExpressionSyntax;
diff --git a/ownlang/diag_sarif.py b/ownlang/diag_sarif.py
index ffdc44f1..e24ff87d 100644
--- a/ownlang/diag_sarif.py
+++ b/ownlang/diag_sarif.py
@@ -10,7 +10,7 @@
and projects its evidence through the SAME ``ownlang.evidence`` builders the OwnIR
path uses (``relatedLocations`` for the unordered anchors, ``codeFlows`` for the
ordered slice), so both paths speak one SARIF vocabulary. The log shape mirrors
-``ownir.build_sarif``: one ``run`` whose ``tool.driver`` is Own.NET with a
+``ownir.build_sarif``: one ``run`` whose ``tool.driver`` is Owen with a
``rules`` catalogue of the OWN codes present, and one ``result`` per diagnostic.
"""
@@ -70,7 +70,7 @@ def _result(d: Diagnostic, filename: str, severity: str) -> dict[str, Any]:
def build_sarif(diags: list[Diagnostic], filename: str,
severity: str = "error") -> dict[str, Any]:
"""Render flow diagnostics as a single SARIF 2.1.0 log: one ``run`` whose
- ``tool.driver`` is Own.NET (with a ``rules`` catalogue of the OWN codes present
+ ``tool.driver`` is Owen (with a ``rules`` catalogue of the OWN codes present
and their titles) and whose ``results`` carry each diagnostic's code, location,
message and evidence slice. ``severity`` only sets each result's ``level``."""
codes = sorted({d.code for d in diags})
@@ -85,7 +85,7 @@ def build_sarif(diags: list[Diagnostic], filename: str,
{
"tool": {
"driver": {
- "name": "Own.NET",
+ "name": "Owen",
"informationUri": _SARIF_INFO_URI,
"rules": rules,
},
diff --git a/ownlang/ownir.py b/ownlang/ownir.py
index 390a6390..ff42d28e 100644
--- a/ownlang/ownir.py
+++ b/ownlang/ownir.py
@@ -465,7 +465,7 @@ def _sarif_result(f: Finding, severity: str) -> dict[str, Any]:
def build_sarif(findings: list[Finding], severity: str = "error") -> dict[str, Any]:
"""Render the findings as a single SARIF 2.1.0 log: one `run` whose
- `tool.driver` is Own.NET (with a `rules` catalogue of the OWN codes present and
+ `tool.driver` is Owen (with a `rules` catalogue of the OWN codes present and
their titles) and whose `results` carry each finding's code, C# location,
message and resource kind. `severity` is the same presentation choice as the
other surfaces (it only sets each result's `level`).
@@ -487,7 +487,7 @@ def build_sarif(findings: list[Finding], severity: str = "error") -> dict[str, A
{
"tool": {
"driver": {
- "name": "Own.NET",
+ "name": "Owen",
"informationUri": _SARIF_INFO_URI,
"rules": rules,
"properties": {"ownirSchemaVersion": OWNIR_VERSION},
diff --git a/tests/test_diag_sarif.py b/tests/test_diag_sarif.py
index 5a1290f0..42462aca 100644
--- a/tests/test_diag_sarif.py
+++ b/tests/test_diag_sarif.py
@@ -96,7 +96,7 @@ def expect(cond: bool, msg: str) -> None:
expect(esc["$schema"].endswith("sarif-schema-2.1.0.json") and esc["version"]
== "2.1.0", "SARIF log must declare the 2.1.0 schema/version")
driver = esc["runs"][0]["tool"]["driver"]
- expect(driver["name"] == "Own.NET", "tool driver must be Own.NET")
+ expect(driver["name"] == "Owen", "tool driver must be Owen")
expect([r["id"] for r in driver["rules"]] == ["OWN015"],
f"rules catalogue must list the codes present: {driver['rules']}")
diff --git a/tests/test_ownir.py b/tests/test_ownir.py
index f37b5619..f915d76e 100644
--- a/tests/test_ownir.py
+++ b/tests/test_ownir.py
@@ -3084,7 +3084,7 @@ def _bcl(body: list) -> list:
fails.append(f"SARIF envelope wrong: version={sf.get('version')!r}")
driver = sf["runs"][0]["tool"]["driver"]
checks += 1
- if driver.get("name") != "Own.NET":
+ if driver.get("name") != "Owen":
fails.append(f"SARIF tool.driver.name wrong: {driver.get('name')!r}")
checks += 1
if [(r["id"], r["shortDescription"]["text"]) for r in driver["rules"]] != \