Skip to content

feat(metamorphic): bridge target — metamorphic testing of check_facts#46

Merged
PhysShell merged 2 commits into
mainfrom
claude/metamorphic-facts
Jun 19, 2026
Merged

feat(metamorphic): bridge target — metamorphic testing of check_facts#46
PhysShell merged 2 commits into
mainfrom
claude/metamorphic-facts

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Adds scripts/metamorphic_facts.py — the sibling of metamorphic.py (#45) one level down. Instead of mutating .own source it mutates the OwnIR facts (the JSON the C# extractor emits) and asserts check_facts's diagnostics are invariant. The facts are sets of records, so reversing any record list or consistently renaming a component/service identifier cannot change which leaks exist — a moved verdict would mean the bridge is order/name-sensitive where it must not be. Higher-signal than the core, since the bridge carries the incidental complexity (DI captive graph, finding dedup, source-lifetime tiering). dotnet-free.

Transforms (both meaning-preserving)

  • reverse — reverse a list of records: the top-level component/service/function lists, a component's resource list, a service's deps. Independent records commute.
  • rename — consistently rename a component/service identifier everywhere it appears as a full string value (references in deps/source_type move with it) — alpha-equivalence over the fact graph.

Compared on the multiset of diagnostic codes (the lesson the core harness learned from codex: a record's line is intrinsic, but order/name must not move a code).

Result

All 18 committed fact fixtures + a captive-DI set are invariant — including DI001 (a singleton capturing a scoped service), which holds under both service reordering and a consistent rename of the dependency graph. No bug found: the heavily-edited bridge (DI graph, dedup, tiering) is order/name-stable — the reassuring outcome.

The analyzer is now metamorphically gated top to bottom, locally: core (.own, 28 programs, metamorphic.py) + bridge (facts, 18 fixtures, metamorphic_facts.py), both --selftests in CI. The OwnIR-fact follow-up in docs/notes/metamorphic.md is now done.

Validation

metamorphic-facts 6/6, metamorphic 8/8, miner 15/15, oracle 24/24, full suite green, ruff + mypy --strict clean, ci.yml parses. Also corrected a stale (code, line) mention left over from #45.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new metamorphic_facts CLI that runs invariance checks by generating transformed OwnIR fact variants and comparing diagnostic code outputs.
  • Documentation

    • Rewrote robustness targets and expanded the “Run it” examples to include metamorphic_facts.py sweeps and selftests.
  • Chores

    • Updated CI to execute the new Python self-test during the existing lint job.

Add scripts/metamorphic_facts.py, the sibling of metamorphic.py one level down:
instead of mutating .own source it mutates the OwnIR facts (the JSON the extractor
emits) and asserts check_facts's diagnostics are invariant. The facts are sets of
records, so reversing any record list or consistently renaming a component/service
identifier cannot change which leaks exist — a moved verdict would mean the bridge
is order/name-sensitive where it must not be.

- Two sound transforms: reverse (top-level component/service/function lists, a
  component's resource list, a service's deps) and rename (a component/service
  identifier, consistently across the whole fact graph by exact-match replace).
- Compared on the code multiset (same lesson the core harness learned from codex).
- Result: all 18 committed fact fixtures + a captive-DI set are invariant,
  including DI001 (a singleton capturing a scoped service) under both service
  reordering and a consistent rename of the dependency graph. No bug found — the
  heavily-edited bridge (DI graph, finding dedup, source-lifetime tiering) is
  order/name-stable, which is the reassuring outcome.
- Wired into the CI script-selftests step; docs/notes/metamorphic.md now covers
  both targets (core .own + bridge facts) and the OwnIR-fact follow-up is done.
  Also corrected a stale "(code, line)" mention left over from #45.

metamorphic-facts 6/6, metamorphic 8/8, miner 15/15, oracle 24/24, full suite
green, ruff + mypy --strict clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d309f12-e912-4091-af16-4e5185ebe7a1

📥 Commits

Reviewing files that changed from the base of the PR and between 5a3a514 and 4e9a032.

📒 Files selected for processing (1)
  • scripts/metamorphic_facts.py

📝 Walkthrough

Walkthrough

Adds scripts/metamorphic_facts.py, a new CLI harness for metamorphic testing of the ownlang.ownir.check_facts bridge. It applies list-reversal and identifier-rename transforms to .facts.json fixtures and asserts that the resulting diagnostic code multisets are unchanged. The CI lint job gains a --selftest step, and the metamorphic testing docs are updated accordingly.

Changes

Bridge metamorphic test harness

Layer / File(s) Summary
Module docstring, code_key, and string utilities
scripts/metamorphic_facts.py
Module docstring declares the invariant contract and CLI. code_key returns a sorted tuple of diagnostic codes from check_facts. _strings traverses the full fact graph to collect string literals. _rename_id deep-copies a fact object while globally substituting a target string only in identifier sites (name, source_type, deps), leaving semantic literals unchanged.
reverse_variants and rename_variants transforms
scripts/metamorphic_facts.py
reverse_variants yields copies with top-level record lists (components, services, functions) and nested subscriptions/deps reversed when their lengths exceed 1. rename_variants iterates each component/service identifier, picks a collision-free name, and yields a fully alpha-renamed copy for each via _rename_id.
Invariance checking and file discovery
scripts/metamorphic_facts.py
violations runs all transform variants, records diagnostic mismatches or unexpected exceptions, and skips OwnIRError on malformed inputs. sweep collects *.facts.json files, loads each through schema validation, reports per-file violations, prints an invariant rate, and returns an exit code driven by failures.
Selftest validation and main CLI
scripts/metamorphic_facts.py
_selftest validates fixture robustness, leak-vs-clean OWN001 discrimination, multi-component/multi-service invariance, rename collision safety, and schema-invalid ownir_version rejection. main routes --selftest vs. sweep operation and wires SystemExit.
CI wiring and documentation
.github/workflows/ci.yml, docs/notes/metamorphic.md
Adds python scripts/metamorphic_facts.py --selftest to the lint job. Rewrites the "result so far" section with separate core and bridge invariance claims, expands "Run it" with new commands, and updates the follow-ups list with bridge-level fact mutations.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~13 minutes

Possibly related PRs

  • PhysShell/Own.NET#36: Main PR's new scripts/metamorphic_facts.py validates that ownlang.ownir.check_facts diagnostic-code outputs stay invariant under fact rewrites, which directly exercises the check_facts() behavior changes introduced in the retrieved PR.

  • PhysShell/Own.NET#45: Both PRs modify the CI lint job's script-selftests to run metamorphic testing (retrieved adds python scripts/metamorphic.py --selftest, main adds python scripts/metamorphic_facts.py --selftest alongside it), directly connected at the CI wiring level.

Poem

🐇 A new script hops in, ready to test,
Renaming and reversing — put facts to the rest!
If codes stay the same through each clever disguise,
The bridge holds its truth, no surprise in its eyes.
Invariants proven, the linter runs clean —
The tidiest burrow this warren has seen! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding metamorphic testing for the check_facts bridge, which is the primary objective of this changeset.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/metamorphic-facts

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a3a514b95

ℹ️ 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".

Comment thread scripts/metamorphic_facts.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
scripts/metamorphic_facts.py (1)

93-97: ⚡ Quick win

Avoid sharing original nested objects when building reverse variants.

Lines 96 and 104 reverse subs/deps taken from the original facts, then assign them into the copied variant. That reintroduces aliases to the source graph and can taint later variants if any downstream path mutates input objects.

Safe local fix
-                v["components"][i]["subscriptions"] = list(reversed(subs))
+                v_subs = v["components"][i].get("subscriptions")
+                if isinstance(v_subs, list):
+                    v["components"][i]["subscriptions"] = list(reversed(v_subs))
                 yield (f"reverse components[{i}].subscriptions", v)
...
-                v["services"][i]["deps"] = list(reversed(deps))
+                v_deps = v["services"][i].get("deps")
+                if isinstance(v_deps, list):
+                    v["services"][i]["deps"] = list(reversed(v_deps))
                 yield (f"reverse services[{i}].deps", v)

Also applies to: 101-105

🤖 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_facts.py` around lines 93 - 97, The code shares nested
object references when assigning reversed subscriptions and dependencies to the
copied variants. In the reversed components subscriptions block (around line 96)
and the similar reversed component dependencies block (around 104-105), instead
of assigning list(reversed(subs)) and list(reversed(deps)) directly, wrap them
with copy.deepcopy to ensure the reversed lists contain independent copies of
their elements. This prevents downstream mutations from tainting the original
facts structure through shared object references.
🤖 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_facts.py`:
- Around line 66-77: The _rename function currently performs exact string
matching and replacement on all string values globally throughout the fact
graph, which causes false positives when unrelated semantic literals (like DI
lifetime values) happen to match an identifier name. Refactor the _rename
function to only apply string renaming when processing identifier-bearing fields
(such as component/service names, deps, source_type, etc.) within dictionary
structures, while leaving string values in other contexts untouched. This
requires checking the dictionary key name when iterating through dict.items()
and only calling _rename on values from identifier-bearing fields, otherwise
returning them unchanged.
- Around line 164-176: The function fails to report errors when input files
cannot be loaded or when no files are processed, allowing exit code 0 in failure
scenarios. In the exception handler that catches OSError and
json.JSONDecodeError (lines 164-166), increment the bad counter to track load
failures alongside violation failures. Additionally, add a check after the file
processing loop to ensure at least one file was successfully processed; if the
files list is empty, return a failure code. This ensures the final return
statement correctly propagates failure status for both unreadable inputs and
empty sweep sets.

---

Nitpick comments:
In `@scripts/metamorphic_facts.py`:
- Around line 93-97: The code shares nested object references when assigning
reversed subscriptions and dependencies to the copied variants. In the reversed
components subscriptions block (around line 96) and the similar reversed
component dependencies block (around 104-105), instead of assigning
list(reversed(subs)) and list(reversed(deps)) directly, wrap them with
copy.deepcopy to ensure the reversed lists contain independent copies of their
elements. This prevents downstream mutations from tainting the original facts
structure through shared object references.
🪄 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: 953348a0-79ed-409b-bbdd-5da2a0fcc5a5

📥 Commits

Reviewing files that changed from the base of the PR and between bfc1c2e and 5a3a514.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • docs/notes/metamorphic.md
  • scripts/metamorphic_facts.py

Comment thread scripts/metamorphic_facts.py Outdated
Comment thread scripts/metamorphic_facts.py Outdated
…ep exit

Addresses codex + CodeRabbit review on #46.

- codex: drive the real bridge entry — check_facts(load(path)). sweep and the
  selftest now load() each fact set (schema + version gate) instead of raw
  json.loads, so a schema-invalid file (e.g. a future ownir_version) is rejected
  at the gate, not silently swept as "invariant". New version-gate selftest case.
- CodeRabbit (major): the rename is now key-aware — it touches only identifier
  sites (a name/source_type field, a deps entry), never a semantic literal, so a
  component/service named e.g. "scoped" no longer has its lifetime rewritten. New
  collision selftest case; all 18 fixtures stay invariant (the narrower rename is
  still complete for the schema's reference graph).
- CodeRabbit (major): sweep returns non-zero on a load error or an empty input set
  (a sweep that evaluated nothing is no longer a false green).
- CodeRabbit (nit): reverse_variants reverses the deep-copied variant's own list,
  not the original's, so a variant never aliases the source graph.

metamorphic-facts 8/8, metamorphic 8/8, miner 15/15, oracle 24/24, full suite
green, ruff + mypy --strict clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@PhysShell
PhysShell merged commit 86dce9e into main Jun 19, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants