Skip to content

test(devtools): guard proofs with production reachability - #3913

Merged
Sinity merged 5 commits into
masterfrom
feature/fix/production-reachability-oracle
Aug 10, 2026
Merged

test(devtools): guard proofs with production reachability#3913
Sinity merged 5 commits into
masterfrom
feature/fix/production-reachability-oracle

Conversation

@Sinity

@Sinity Sinity commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

Add a structured production-reachability oracle for proof tests. The oracle verifies that declared production seams call the tested production symbol and that the symbol is reachable from the declared production entrypoint.

Problem

The previous proof gate could certify orphan helpers, over-approximate routes through nested function bodies or passed callable arguments, and resolve Python imports/caches incorrectly. The hermetic filesystem-boundary half remains a named successor.

Solution

  • Build a Python call graph with correct nested-callable, import, shadowing, package-relative, dotted-import, and class-method semantics.
  • Reuse parsed production modules while invalidating the cache when source files change.
  • Add mutation-oriented fixtures for the false-positive classes found in exact-head review.
  • Keep the existing production-reachability fixture and polylogue-jdesf successor for hermetic path boundaries.

Verification

  • devtools test tests/unit/devtools/test_production_reachability.py — 7 passed.
  • devtools verify --quick — all 24 steps exit 0 at 93ab11703c87ac777b9c8513dfa764be5e1a7de1.

Scope and residuals

This PR is partial for polylogue-4v2d3; it does not claim hermetic test-path enforcement, which remains polylogue-jdesf. It does not mutate production or the live archive.

Ref #4v2d3

Sinity and others added 3 commits August 10, 2026 09:44
Problem: a proof test can call a helper that is no longer on the production route and still certify archive behavior.

What changed: add an AST import/call graph oracle with structured seam metadata, a red unreachable-helper fixture, and bindings for the three reindex replay/convergence proof tests. The bindings require the real rebuild entrypoint to reach raw replay and terminal insight convergence.

Compatibility/migration: verification-only. No production code or archive state changes.

Ref polylogue-4v2d3
Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added an AST-based production reachability checker with structured diagnostics. Added fixture tests for reachable and unreachable symbols. Integrated seam checks into selected rebuild tests.

Changes

Production Reachability Verification

Layer / File(s) Summary
Seam contracts and reports
devtools/production_reachability.py
Added public seam specifications, violation records, reachability reports, JSON serialization, and exports.
AST call-graph construction
devtools/production_reachability.py
Added source discovery, AST parsing, import resolution, call-target resolution, nested-call filtering, and cached call-graph loading.
Seam validation and diagnostics
devtools/production_reachability.py
Added checks for production entrypoints, test functions, direct symbol calls, reachable symbols, required symbols, and JSON assertion failures.
Fixture and rebuild verification
tests/fixtures/production_reachability/*, tests/unit/devtools/test_production_reachability.py, tests/unit/maintenance/test_rebuild_parse_apply_split.py, .beads/issues.jsonl
Added fixture routes, package-import cases, unit tests, rebuild seam specifications, repository seam validation, and the related development task.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TestSeam
  participant check_production_seam
  participant ASTCallGraph
  participant ProductionReachabilityReport
  TestSeam->>check_production_seam: submit ProductionSeamSpec
  check_production_seam->>ASTCallGraph: load production and test sources
  ASTCallGraph-->>check_production_seam: return reachable symbols
  check_production_seam->>ProductionReachabilityReport: record seam violations
  ProductionReachabilityReport-->>TestSeam: return validation status and JSON diagnostics
Loading

Possibly related PRs

  • Sinity/polylogue#3885: Implements inactive-candidate rebuild isolation for paths covered by the new production-reachability checks.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly identifies the test change that adds production-reachability protection for devtools proofs.
Description check ✅ Passed The description covers the change, problem, solution, verification, scope disposition, residual work, and issue reference; omitted template sections are non-critical for this test-only change.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fix/production-reachability-oracle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@Sinity

Sinity commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@circleci run

@Sinity

Sinity commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

🤖 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 `@devtools/production_reachability.py`:
- Around line 110-130: Update _index_edges to traverse only the current function
scope, excluding nested FunctionDef, AsyncFunctionDef, and lambda bodies when
collecting calls for each enclosing function. Preserve calls made directly by
the current function and add a regression fixture where an uninvoked nested
function calls a live helper without creating an edge from the enclosing
function.
- Around line 171-191: Preserve the package-initializer state from _ParsedModule
and pass it into the relative-import resolution flow. Update
_resolve_relative_module and its callers, including _imports_from_nodes, so
level-one imports from package __init__.py files retain the package name (for
example, package.child), while existing relative-import behavior for regular
modules remains unchanged.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e05b2520-e7fb-4172-9601-eed5f4f42493

📥 Commits

Reviewing files that changed from the base of the PR and between 7b89000 and c1d2871.

📒 Files selected for processing (7)
  • .beads/issues.jsonl
  • devtools/production_reachability.py
  • tests/fixtures/production_reachability/__init__.py
  • tests/fixtures/production_reachability/fixture_test.py
  • tests/fixtures/production_reachability/routes.py
  • tests/unit/devtools/test_production_reachability.py
  • tests/unit/maintenance/test_rebuild_parse_apply_split.py

Comment thread devtools/production_reachability.py
Comment thread devtools/production_reachability.py Outdated

@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: c1d2871cd4

ℹ️ 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 devtools/production_reachability.py Outdated
Comment on lines +124 to +127
references = [call.func, *call.args, *(keyword.value for keyword in call.keywords)]
for reference in references:
for expression in ast.walk(reference):
target = _resolve_call_target(expression, bindings, self.nodes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Only create test edges for invoked callees

When a proof test merely passes a production symbol as an argument, such as assert callable(production_entrypoint), this loop resolves the argument and records it as a call target even though the symbol is never invoked. check_production_seam therefore reports the test as production-wired, allowing exactly the vacuous proof this guard is meant to reject; test-call edges should come from each ast.Call.func, not arbitrary argument expressions.

Useful? React with 👍 / 👎.

Comment thread devtools/production_reachability.py Outdated
Comment on lines +121 to +127
for call in ast.walk(function.node):
if not isinstance(call, ast.Call):
continue
references = [call.func, *call.args, *(keyword.value for keyword in call.keywords)]
for reference in references:
for expression in ast.walk(reference):
target = _resolve_call_target(expression, bindings, self.nodes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude uninvoked nested bodies from caller edges

When an entrypoint defines a nested function or lambda that calls a required symbol but never invokes that closure, ast.walk(function.node) still descends into the nested body and attributes its calls directly to the entrypoint. The required symbol is consequently reported reachable despite having no executable path from the declared root, so traversal needs to stop at nested function, lambda, and class scopes and analyze those as separate nodes where appropriate.

Useful? React with 👍 / 👎.

Comment on lines +181 to +184
if isinstance(node, ast.Import):
for alias in node.names:
bindings[alias.asname or alias.name.split(".")[0]] = alias.name
elif isinstance(node, ast.ImportFrom):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Model unaliased dotted imports with Python binding rules

For a valid import such as import prod.routes, Python binds the local name prod, but this code binds prod to prod.routes; resolving prod.routes.production_entrypoint() then constructs prod.routes.routes.production_entrypoint and incorrectly emits test_symbol_not_called. Any proof test using the standard unaliased dotted-import form is therefore rejected, so unaliased imports should bind the first component to itself while preserving the full path only for an explicit alias.

Useful? React with 👍 / 👎.

Comment on lines +203 to +210
def _resolve_call_target(function: ast.AST, bindings: dict[str, str], nodes: dict[str, _FunctionNode]) -> str | None:
parts = _attribute_parts(function)
if parts is None or not parts:
return None
bound = bindings.get(parts[0])
if bound is None:
return None
target = ".".join((bound, *parts[1:]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve instance-method calls across class routes

When a declared route is a class method, calls such as Runner().production_entrypoint() and self.live_helper() cannot be resolved because _attribute_parts only accepts name/attribute chains and self has no binding. Although class methods are indexed as nodes, a valid class-based production path is therefore reported with test_symbol_not_called and required_symbol_unreachable; the graph needs class/instance context for constructor and self/cls dispatch.

Useful? React with 👍 / 👎.

Comment thread devtools/production_reachability.py Outdated
Comment on lines +226 to +230
@lru_cache(maxsize=8)
def _call_graph(source_root: Path, test_path: Path) -> _CallGraph:
production_root = source_root / "polylogue"
graph_roots = (production_root if production_root.is_dir() else source_root, test_path)
return _CallGraph(_parse_modules(source_root, graph_roots))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invalidate cached graphs when source files change

When a mutation test or long-lived verifier edits a production file and calls check_production_seam again for the same root and test path, this cache returns the pre-edit AST because its key contains no source fingerprint or mtime. Removing a required call after an initial green check therefore still produces a green report in the same process, undermining controlled dead-symbol mutation coverage; cache entries must incorporate source state or be explicitly invalidated before each verification run.

Useful? React with 👍 / 👎.

Comment on lines +118 to +120
for function in tuple(node for node in self.nodes.values() if node.module == module.name):
bindings = {**module_imports, **local_functions, **_imports_from_nodes(function.node.body, module.name)}
targets: set[str] = set()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Account for local shadowing before resolving calls

When a test parameter or local assignment shadows an imported production symbol, the bindings table still maps that name to the import because parameters and assignments are never analyzed. For example, a test that assigns production_entrypoint = lambda: None and invokes that lambda is reported as directly calling the real production entrypoint, so a test double or fixture can produce a false-green reachability proof; lexical bindings must override module imports before call targets are resolved.

Useful? React with 👍 / 👎.

Comment thread devtools/production_reachability.py Outdated
Comment on lines +226 to +230
@lru_cache(maxsize=8)
def _call_graph(source_root: Path, test_path: Path) -> _CallGraph:
production_root = source_root / "polylogue"
graph_roots = (production_root if production_root.is_dir() else source_root, test_path)
return _CallGraph(_parse_modules(source_root, graph_roots))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reuse one production graph across test files

As soon as seam specs span multiple test files, test_path makes every cache entry parse and retain a separate full AST/call graph for the entire polylogue package. The eight-entry cache can therefore hold eight near-identical repository-sized graphs, causing verification latency and memory usage to scale with test-file count rather than changed code; cache the production graph once per source state and layer each small test module onto it.

Useful? React with 👍 / 👎.

@Sinity

Sinity commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@circleci run

@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: 1

🤖 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 `@devtools/production_reachability.py`:
- Around line 306-309: Update _calls_in_function to visit only function.body
rather than the complete FunctionDef or AsyncFunctionDef, excluding decorators
and default-value expressions from call indexing. Add a regression fixture where
a decorator or default argument invokes dead_helper, and verify that dead_helper
is not marked reachable from the entrypoint.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5d6bc346-772c-4f5d-9e64-ade9291727ac

📥 Commits

Reviewing files that changed from the base of the PR and between c1d2871 and 93ab117.

📒 Files selected for processing (6)
  • devtools/production_reachability.py
  • tests/fixtures/production_reachability/fixture_test.py
  • tests/fixtures/production_reachability/nestedpkg/__init__.py
  • tests/fixtures/production_reachability/nestedpkg/child.py
  • tests/fixtures/production_reachability/routes.py
  • tests/unit/devtools/test_production_reachability.py

Comment thread devtools/production_reachability.py
@Sinity
Sinity merged commit f82c1c9 into master Aug 10, 2026
2 of 3 checks passed
@Sinity
Sinity deleted the feature/fix/production-reachability-oracle branch August 10, 2026 09:04

@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: fc0aac908f

ℹ️ 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 on lines +236 to +237
if bound in nodes or any(name.startswith(f"{bound}.") for name in nodes):
return bound

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require the tested function itself to be invoked

When a proof calls an attribute of an imported production function, such as production_entrypoint.__str__(), the constructed attribute target is absent but this fallback returns the bound function itself. The report therefore records a direct call to production_entrypoint even though only its attribute ran, allowing a passing test to satisfy tested_symbols without executing the production entrypoint; the fallback to bound should apply only to constructor calls represented by a bare name.

Useful? React with 👍 / 👎.

Comment on lines +294 to +295
for statement in node.body:
self.visit(statement)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude statically unreachable calls from production edges

When a required helper is mentioned only under if False: or after an unconditional return, this unconditional statement walk still records the call and reports the helper reachable. Removing the real production invocation can therefore leave the proof green whenever a dead reference remains, defeating the controlled dead-symbol mutation this oracle is intended to enforce; the scanner should at least prune statically false branches and statements after unconditional terminators.

Useful? React with 👍 / 👎.

Comment on lines +103 to +106
for statement in module.tree.body:
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
qualified_name = f"{module.name}.{statement.name}"
self.nodes[qualified_name] = _FunctionNode(qualified_name, statement, module.name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Index public re-export aliases as production nodes

When a production entrypoint is re-exported from a package, the index contains only the defining function and never the public alias. For example, polylogue/daemon/__init__.py re-exports polylogue.daemon.cli.main as polylogue.daemon.main, so a proof that imports and declares the public daemon entrypoint receives missing_production_entrypoint and missing_tested_symbol even though that is the runtime API; imported callable aliases need to resolve to their defining nodes.

Useful? React with 👍 / 👎.

Comment on lines +308 to +310
def visit_Call(self, node: ast.Call) -> None:
self.calls.append(node)
self.generic_visit(node)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require asynchronous calls to be awaited

When a test merely creates and closes a coroutine with pending = async_entrypoint(); pending.close(), this visitor records the call and treats the async function's body as reachable even though none of it executed. The same false green occurs inside production when an async required symbol is called without await, so the oracle can certify a route whose required work never runs; edges to AsyncFunctionDef nodes should require an awaited call context.

Useful? React with 👍 / 👎.

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.

1 participant