@@ -87,43 +87,76 @@ Per the guardrail this rebrand was scoped to: no mass rename.
8787 the help text does not claim support that doesn't exist yet.
8888- ** Unsupported input fails explicitly.** Before this rebrand, pointing
8989 ` check ` at a ` .ts ` file or an empty/non-C# directory silently printed
90- "0 findings" — indistinguishable from a genuinely clean C# scan.
91- ` CheckCommand.HasSupportedInput ` now checks every given path resolves to
92- something the included frontend can read (a ` .cs ` /` .csproj ` /` .sln ` file,
93- or a directory containing at least one ` .cs ` file anywhere under it)
94- * before* running the extractor, and exits ** 4** with an explicit message
95- if none do. This is a new, additive exit-code tier — it does not change
96- any of the existing 0/1/` >=2 ` /3 contract. ** Correction (review, PR #246 ):**
97- the directory walk in this preflight check now uses the same
98- `EnumerationOptions { RecurseSubdirectories = true, IgnoreInaccessible =
99- true }` the extractor's own ` Expand()` walk uses ( ` OwnSharp.Extractor/
100- Program.cs` ) — the first version used a plain ` SearchOption.AllDirectories`,
101- which throws on a locked/permission-denied subdirectory instead of
102- tolerating it and continuing, regressing a directory scan the extractor
103- itself would have handled fine.
90+ "0 findings" — indistinguishable from a genuinely clean C# scan. The
91+ explicit-failure behavior now has two layers, split across the two
92+ components that each know a different half of the answer (review, PR
93+ #246 round 2): ` CheckCommand.HasSupportedInput ` does only the CHEAP,
94+ obvious check — an existing file with a recognized extension, or an
95+ existing directory — * before* running the extractor at all, catching a
96+ bare ` .ts ` file or a nonexistent path for free. ` SupportedExtensions ` is
97+ compared with ` StringComparer.OrdinalIgnoreCase ` (review, PR #246 round
98+ 2: ` Foo.CS ` /` App.CSPROJ ` are the same file kind as their lowercase
99+ spellings, on Windows/macOS filesystems and to MSBuild itself). The CLI
100+ does ** not** also try to duplicate the extractor's directory-walk skip
101+ rules to predict whether a directory will actually yield anything — a
102+ directory containing only ` bin/ ` , ` obj/ ` , or generated (` .g.cs ` ,
103+ ` .Designer.cs ` , ` .AssemblyInfo.cs ` ) files passed the CLI's cheap check
104+ fine but produced zero real extractor inputs, recreating the exact
105+ silent-clean-scan bug this exit tier exists to prevent. Duplicating the
106+ extractor's skip-list in the CLI would just drift from it over time
107+ (review, PR #246 round 2 explicit instruction: "do not duplicate the
108+ complete extractor expansion rules in the CLI"). Instead, the extractor
109+ itself is now the sole authority on "found nothing after expansion":
110+ ` OwnSharp.Extractor/Program.cs ` checks ` Expand(rawInputs).Distinct() `
111+ immediately after computing it and returns a new exit code ** 4** with an
112+ explicit message if that list is empty — a ` .csproj ` /` .sln ` with no
113+ usable source resolves the same way, since project/solution resolution
114+ is text/glob-based, not full MSBuild evaluation (see the ` ProjectCsFiles `
115+ comment). ` CheckCommand.RunAsync ` 's existing ` extractRc != 0 ` early
116+ return propagates that 4 unchanged, and ` own-check.sh ` 's existing `set
117+ -e` does the same for the script/Action path — no extra code needed on
118+ either side beyond the extractor's own check. This is a new, additive
119+ exit-code tier; it does not change any of the existing 0/1/` >=2 ` /3
120+ contract.
104121- ** ` OWEN_PYTHON ` ** is the preferred env var; the legacy ** ` OWN_PYTHON ` **
105122 name is still accepted as a temporary compatibility fallback (so existing
106123 internal use — CI, scripts, muscle memory — doesn't break outright) and
107124 prints a one-line deprecation note to stderr every time it's the variable
108125 actually used to resolve Python. ` OWEN_PYTHON ` takes priority when both
109126 are set.
110- - ** Cache directory** moved to ` ~/.owen/core/<version>/ ` . ` CoreVendor ` checks
111- the previous ` ~/.ownsharp/core/<version>/ ` location first — if that exact
112- version was already unpacked there by an older install, it's reused in
113- place rather than re-copied. This is a fallback read, not a migration
114- subsystem: the old location is never written to, moved, or deleted by the
115- new code. ** Correction (review, PR #246 ):** the first version of this
116- fallback trusted a bare ` .unpacked ` marker file's * existence* — but the
117- CLI's own ` <Version> ` doesn't change every time the vendored core's
118- content does (this rebrand's own SARIF-driver-name change is the concrete
119- proof: same ` 0.1.0 ` , different core content), so an existence-only check
120- could silently reuse a stale, differently-vendored core from a machine
121- that had run a pre-rebrand ` OwnSharp.Cli ` install. ` CoreVendor ` now writes
122- a SHA-256 fingerprint of the bundled ` ownlang/*.py ` files as the marker's
123- content (both locations) and only trusts a cache whose marker's hash
124- matches the source this install would unpack right now — same "simple,
125- not a migration subsystem" shape, just content-aware instead of
126- version-only.
127+ - ** Cache directory** moved to ` ~/.owen/core/<version>/<fingerprint>/ ` ,
128+ where ` fingerprint ` is a SHA-256 over every vendored file's name and
129+ content (length-prefixed encoding, so e.g. name ` "ab" ` + content ` "c" `
130+ can't collide with name ` "a" ` + content ` "bc" ` by bare concatenation).
131+ ** Correction (review, PR #246 round 3):** the round-2 design (a
132+ version-keyed directory with a separate marker file holding a source
133+ fingerprint) had a gap the reviewer's reproduction demonstrated directly:
134+ on a fingerprint mismatch, the fix copied new files over the existing
135+ destination with ` overwrite: true ` , then rewrote the marker to match the
136+ new source — but a file the new source no longer has (e.g. a module
137+ deleted upstream) was never removed from the destination, so it survived
138+ as an orphan while the rewritten marker now claimed the (polluted)
139+ directory was valid. Round 3 removes the marker concept entirely and
140+ makes cache identity content-addressed: the fingerprint * is* the last
141+ path segment, so a content change is a different path, never an
142+ in-place overwrite of an existing one. Publication into a fresh path is
143+ atomic — build into a ` .tmp-<guid> ` sibling directory, recompute the
144+ fingerprint of what was actually written there, confirm it equals the
145+ source fingerprint, and only then ` Directory.Move ` it to the final
146+ fingerprint-named path — so a reader can only ever observe nothing, or a
147+ fully-written self-verified copy, never a partial one (a crash or a lost
148+ race with a concurrent ` owen ` process mid-copy just leaves a harmless
149+ orphaned temp directory that nothing consults). The previous flat
150+ ` ~/.ownsharp/core/<version>/ ` location (pre-rebrand layout, no
151+ fingerprint segment) is still checked first and used in place without
152+ copying — but only after fingerprinting what is * actually on disk* there
153+ right now and confirming it equals the current source fingerprint; a
154+ destination with extra, missing, or modified files fails that check and
155+ falls through to a fresh unpack instead of being trusted or patched.
156+ This is still a plain fallback * read* : the legacy location is never
157+ written to, moved, or deleted by this code — content-addressing didn't
158+ change that guardrail, only how "is this destination actually still
159+ correct" gets decided.
127160
128161## Tests
129162
@@ -145,6 +178,44 @@ does **not** contain the literal clean-scan phrase `0 findings.`),
145178by the driver-name change, not scope creep; they were caught by
146179` python tests/run_tests.py ` failing before the fix.
147180
181+ ** Added in review round 3** — the reviewer's stated concern was that both
182+ bugs (stale-cache pollution, cheap-preflight/extractor disagreement) were
183+ "sufficiently non-obvious that a future cleanup could reintroduce [ them]
184+ while the ordinary fresh-install smoke remains green," so both
185+ reproductions from the review became committed ` ci.yml ` steps rather than
186+ staying local manual verification. All of the below ran locally first
187+ (pack -> isolated-feed install -> exercise -> confirm exit code/output)
188+ before being encoded as assertions:
189+ - ** Matching legacy cache is reused in place** — the legitimate case: a
190+ byte-for-byte-identical legacy ` ~/.ownsharp/... ` unpack is used without
191+ copying (` ~/.owen ` is asserted to * not* get created).
192+ - ** Extra (removed-upstream) file in the legacy cache is rejected** — the
193+ exact reproduction from the review: an old cache holds a file the new
194+ source no longer has. A fresh, clean ` ~/.owen ` unpack must result, with
195+ the stale file absent from it.
196+ - ** Modified-content legacy cache (same filenames, different bytes) is
197+ rejected** — same version, same file set, different content: the
198+ same-version class of bug the fingerprint exists to catch generally, not
199+ just the removed-file case.
200+ - ** Directory containing only skipped files** (` bin/ ` /` obj/ ` ) and
201+ ** directory containing only generated files** (` *.g.cs ` ) both assert
202+ exit 4 with an explicit "no supported input" message — the CLI's cheap
203+ preflight passes these (a directory that exists), and only the
204+ extractor's own zero-expanded-input check catches them.
205+ - ** Empty ` .csproj ` ** (no ` Compile ` items resolve to any ` .cs ` file) and
206+ ** ` .sln ` with no ` Project( ` lines** both assert exit 4 the same way.
207+ - ** Skipped files alongside one real source file** asserts exit 1 with
208+ ` OWN001 ` still found — proving the skip rules don't over-reject a
209+ directory that has genuine content alongside the noise.
210+ - ** An inaccessible subtree alongside one readable source file** (`chmod
211+ 000` on a subdirectory, meaningful on the hosted runners' non-root
212+ accounts unlike this session's root-based local sandbox) asserts no
213+ crash and the readable source's finding is still reported — the
214+ ` EnumerationOptions { IgnoreInaccessible = true } ` tolerance from review
215+ round 2, now with a real permission-denied subdirectory to exercise it.
216+ - ** Uppercase extension (` Leak.CS ` )** asserts exit 1 with ` OWN001 ` found —
217+ the ` StringComparer.OrdinalIgnoreCase ` fix from review round 3.
218+
148219## PR separation
149220
150221This is PR 1 of 3 for the release-readiness work:
0 commit comments