feat(metamorphic): analyzer-robustness harness for the .own core#45
Conversation
Add scripts/metamorphic.py: generate semantically-equivalent variants of a .own program and assert the checker's diagnostics are invariant. A divergence is a robustness bug in the analyzer (it keyed on a name or textual order), found with no labels and no oracle — the StaAgent/Statfier line of testing the analyzer itself. Standalone analyzer QA; it is also the behavior-preservation half of a future RLVR reward, but it pays off today without any LLM. - parse -> mutate the AST (dataclasses.replace; nodes are frozen) -> check_module -> compare the (code, line) multiset. dotnet-free; drives the same parser + core as the CLI. - Two provably meaning-preserving transforms: alpha-rename (a local bound exactly once, no shadowing, + all refs) and reorder (adjacent simple statements whose touched-variable sets are disjoint). Conservative — never emits an unsound variant. - Result: the whole .own corpus (28 programs) is invariant under both — the expected baseline for a symbol-identity/dataflow core. A teeth test pins that the (code,line) key distinguishes a leak from a clean run and that the transforms actually fire, so the invariance result is not vacuous. - Wired into the CI script-selftests step beside the miner/oracle. - docs/notes/metamorphic.md: rationale, the invariant, and the follow-ups where bug-finding grows (more transforms; the OwnIR-fact target; the CI-only C#-source target where the syntactic-FP bugs actually lived). ruff clean; mypy --strict clean; full suite green; metamorphic 6/6, miner 15/15, oracle 24/24. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds ChangesMetamorphic Testing Harness
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f23a3cf851
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
scripts/metamorphic.py (1)
282-285: ⚡ Quick win
sweep()docstring and return contract diverge.The docstring says “Returns the number of files with a violation,” but Line 304 returns exit status (
0/1). Aligning this avoids misuse by callers.Suggested fix
def sweep(paths: list[str]) -> int: - """Run the harness over every .own file under the given files/dirs. Returns the - number of files with a violation (0 == all robust).""" + """Run the harness over every .own file under the given files/dirs. + Returns process exit status: 0 if all files are invariant, else 1."""Also applies to: 304-304
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/metamorphic.py` around lines 282 - 285, The docstring for the sweep() function claims it returns the number of files with a violation, but the actual return statement at line 304 returns an exit status (0 or 1) instead. Update the docstring to accurately describe the actual return behavior, clarifying that it returns 0 if all files are robust and 1 if any violations are found, rather than claiming it returns a count of violated files.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/metamorphic.py`:
- Around line 316-317: The roots variable excludes the gallery directory despite
the PR documentation stating that the selftest sweep should cover gallery,
examples, and corpus. Add repo / "gallery" to the roots list alongside repo /
"examples" and repo / "corpus" to ensure all three directories are included when
collecting .own files via the rglob operation.
- Around line 264-267: The try-except block catching ParseError and LexError
exceptions in the parse function call is returning an empty list, which makes
parsing failures indistinguishable from successful parses with no violations.
This allows _selftest to pass even when corpus files become unparseable. Instead
of returning an empty list when these exceptions occur, either re-raise the
exception or return a distinct value that clearly indicates a parsing error
happened, so that parsing failures are properly detected and reported rather
than silently treated as non-violations.
---
Nitpick comments:
In `@scripts/metamorphic.py`:
- Around line 282-285: The docstring for the sweep() function claims it returns
the number of files with a violation, but the actual return statement at line
304 returns an exit status (0 or 1) instead. Update the docstring to accurately
describe the actual return behavior, clarifying that it returns 0 if all files
are robust and 1 if any violations are found, rather than claiming it returns a
count of violated files.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 98f0d909-20c2-4202-bb28-ba6642f30475
📒 Files selected for processing (3)
.github/workflows/ci.ymldocs/notes/metamorphic.mdscripts/metamorphic.py
…check Two real soundness bugs codex caught on #45, plus CodeRabbit hardening: - Compare the multiset of diagnostic CODES, not (code, line). A sound reorder can legitimately move an end-of-function diagnostic's line (it anchors to the last statement), so including the line false-positived a *correct* checker. Pinned by a regression case (a leak with trailing independent statements). - alpha-rename now rewrites the identifier inside an opaque if/while cond_text too (word-boundary), so a renamed variable used in a condition yields a valid alpha-rename instead of a variant referencing a vanished name. Pinned by a cond-rename case. - selftest parse-checks every corpus file (a file that stops parsing is now a loud failure, not a silent "invariant"); the corpus sweep folds into one check. - sweep() docstring corrected (returns a 0/1 exit status, not a count). Rejected CodeRabbit's "selftest omits gallery": examples/ contains gallery/ and rglob is recursive, so the 28-file sweep already includes the 11 gallery files; repo/"gallery" does not exist. metamorphic 8/8, miner 15/15, oracle 24/24, full suite green, ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
Adds
scripts/metamorphic.py— a metamorphic testing harness for the Own.NET core checker (analyzer QA; the next backlog brick after SARIF). It generates semantically-equivalent variants of a.ownprogram and asserts the checker's diagnostics are invariant. A divergence is a robustness bug in the analyzer itself (it keyed on a name or textual order) — found with no labels and no oracle (the StaAgent / Statfier line). Standalone value, no LLM; it is also the behavior-preservation half of a future RLVR reward (seeresearch-landscape-2026.md), but that is a bonus.How
parse(.own)→ mutate the AST (dataclasses.replace; nodes are frozen) →check_module→ compare the (code, line) multiset. dotnet-free — drives the same parser + core as the CLI. The (code, line) key is the right invariant: the v1 transforms can only move a caret column or keep each node's own line, so a correct checker yields the identical set.Two provably meaning-preserving transforms:
Result
The whole
.owncorpus (gallery + examples + corpus, 28 programs) is invariant under both — the expected baseline for a symbol-identity/dataflow core, now ratcheted in CI. The harness is not vacuous: a teeth test pins that the (code, line) key distinguishes a leak from a clean run and that the transforms actually fire.Honest scope
v1 confirms core robustness rather than finding a bug (the core is principled — name/order-agnostic by design). Bug-finding power grows on the follow-ups in
docs/notes/metamorphic.md: more sound transforms; an OwnIR-fact target (mutate the JSON facts →check_facts; higher-signal, still dotnet-free); and a C# source target (where the P-014 syntactic-FP bugs actually lived — but needs Roslyn, so CI-only).Validation
metamorphic --selftest6/6 (28 files swept), wired into the CIscript selftestsstep beside the miner/oracle;ruffclean,mypy --strictclean (incl. the new script), full suite green, miner 15/15, oracle 24/24.🤖 Generated with Claude Code
https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
Generated by Claude Code
Summary by CodeRabbit
Testing
Documentation
Chores
--selftestmode and file/directory sweep capability.