Skip to content

fix(OMN-15395): decide NewTopic policy-resolution at the call site, not the module — AST provenance guard - #2554

Merged
jonahgabriel merged 1 commit into
devfrom
jonah/omn-15395-r3-createtopics-guard-ast
Jul 30, 2026
Merged

jonahgabriel merged 1 commit into
devfrom
jonah/omn-15395-r3-createtopics-guard-ast

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Jul 30, 2026 •

Copy link
Copy Markdown
Collaborator

OMN-15395 — remediation round 4

Fixes the MEDIUM adversarial review raised against PR #2552 (merged, f420d5fa). #2552's branch was already merged, so this lands on a fresh branch off dev (f420d5fa) — the same pattern #2546 → #2550 → #2552 used.

Headline: the static guard #2552 added to close the second CreateTopics path decided "does this site resolve through the policy?" at module scope. It could not see an unresolved value at a site inside a policy-aware module — which is the exact defect class this lineage keeps shipping.

The defect

_raw_create_topics_offenders computed, once per FILE:

resolves = ("ModelTopicProvisioningPolicy" in text
            and _POLICY_RESOLVER_RE.search(text))

So every NewTopic(...) in any module that mentions the policy anywhere got a blanket pass unless its RF was an integer literal. service_topic_manager.py mentions ModelTopicProvisioningPolicy 5× and calls a resolver 5×, and today carries 3 NewTopic sites under that blanket pass — a 4th raw site there would have been invisible.

The PR body of #2552 claimed the guard "scans src/ and scripts/ for any NewTopic construction that bypasses the policy or hardcodes a literal RF". The "bypasses the policy" arm did not exist as described.

Proven by execution, not inferred

Both guards run against the reconstructed b2ca4faa (#2543) tree (git show b2ca4faa:<path> into a scratch tree):

SHIPPED (#2552) module-scope guard:
  scripts/create_kafka_topics.py:329: module resolves no replication policy
                                      ← 1 offender; service_topic_manager.py MISSED

THIS PR (AST, call-site scope):
  omnibase_infra/service_topic_manager.py:555: replication_factor is not policy-resolved at this call site
  omnibase_infra/service_topic_manager.py:758: replication_factor is not policy-resolved at this call site
  scripts/create_kafka_topics.py:329:          replication_factor is not policy-resolved at this call site
                                      ← 3 offenders

service_topic_manager.py:758 at that commit read replication_factor=config.replication_factor — the raw, unresolved value handed straight to NewTopic, with resolve_replication_factor called three lines above and its result discarded. That is this same OMN-15395 lineage's own round-3 defect-3 / finding-4, and the shipped guard returned nothing for it.

Site :555 is a conservative flag, disclosed rather than hidden: at b2ca4faa, _resolve_specs_for_creation returned tuple(resolved), tuple(sorted(refused)) where resolved is a mutated accumulator list, which the analysis deliberately does not trace (see "Deliberate conservatism" below). The live tree does not use that shape.

The fix

Admissibility is computed from the NewTopic call's own argument expression, by AST provenance:

Rule Effect
Provenance is seeded only by a call to resolve_spec / resolve_specs_for_creation / resolve_replication_factor nothing else can mint an admissible RF
Propagated through attribute, subscript, iteration, method call on a resolved receiver, container literal/comprehension, single-arg builtin repackaging (tuple(...)) the five live shapes stay admissible
A module-local wrapper qualifies by its body (every return resolved), never by its name TopicProvisioner._resolve_spec / _resolve_specs_for_creation pass; a stub def _resolve_spec(s): return s does not
Name lookup is line-ordered per lexical scope (nearest preceding binding wins), with comprehension scopes and PEP 572 walrus hoisting modelled managed_staging_topic_checker binds spec twice — a raw walrus and the resolved mapping — and only the resolved one reaches NewTopic
RF read from keyword or positional slot 3 (both the aiokafka and confluent_kafka signatures) NewTopic('t', 6, 1) is now caught
Absent RF, or a **kwargs splat, is reported an unreadable site is refused, not waved through

Test-only change. No src/ or scripts/ behaviour is modified — the live tree returns [] under the new guard, so all five real NewTopic sites remain admissible.

Controls

7 positive (test_create_topics_guard_sees_a_planted_third_path) — flat literal; unresolved caller-supplied; policy-aware module with a raw site (the one this PR exists for); stub helper named like a resolver; positional literal; **kwargs splat; omitted RF.

6 negative (test_create_topics_guard_accepts_a_policy_resolved_site) — one per live creation shape: direct resolve_spec; resolved scalar from resolve_replication_factor; comprehension over a batch; dict-comprehension + .get() shadowing a raw walrus; module-local wrapper that genuinely delegates; tuple(...)-repackaged batch. Without these, "tighten the guard" degenerates into "flag everything", which is as useless as the blanket pass it replaces.

1 pinned limitation (test_create_topics_guard_is_conservative_about_accumulators) — see below.

RED-before evidence (mutation proof, executed)

_raw_create_topics_offenders reverted in place to the #2552 module-scope body, guarding tests re-run:

[RED (correct)] M-R2: admissibility decided at MODULE scope   12 failed, 3 passed

  policy-aware-module-raw-site        assert len(offenders) == 1 → assert 0 == 1
                                      ← the module-scope guard returns ZERO offenders
                                        for a raw RF inside a policy-aware module

The other 11 failures are the remaining positive/negative controls, which the module-scope predecessor also gets wrong (in both directions). GREEN after: 60 passed in the file, 609 passed across tests/unit/event_bus/.

Deliberate conservatism (disclosed, pinned, not a silent gap)

Provenance is not tracked through a mutated accumulator (out = [] … out.append(policy.resolve_spec(spec))). That is genuinely correct code and the guard reports it anyway. The documented remedy is the batch helper resolve_specs_for_creation — which is what every live path already does — not a loosening of the analysis. test_create_topics_guard_is_conservative_about_accumulators pins the behaviour so the next person to hit it reads it as a decision rather than a bug to widen away. A guard that is loose in order to avoid inconveniencing a refactor is the failure this one replaces.

Acceptance-criteria mapping — re-verified, and where the record stands

#2550's body asserted (a) and (b) as satisfied; review proved both false at repository scope, and #2552 closed them in code. This PR does not re-open them — it closes the gap in the mechanism that certifies them.

AC Status after #2552 Status after this PR Executed evidence
(a) explicit contract-driven RF, no flat RF default on any path True in code. Flag deleted; CLI routed through the policy. But the guard certifying it could only see literal RFs — a non-literal flat default inside a policy-aware module was invisible. True, and now mechanically certified. Every NewTopic RF must trace to a resolver call at its own site. test_every_create_topics_site_resolves_through_the_policy (live tree → []); guard executed against b2ca4faa → 3 offenders vs the predecessor's 1
(b) RF1 in managed staging rejected fail-closed before any CreateTopics True on all three paths in code. The guard's "bypasses the policy" arm was module-scoped, so a regression on any of service_topic_manager.py's 3 sites would not have been caught. True, and the regression path is closed. A 4th raw site in that module is now visible. policy-aware-module-raw-site control (RED under M-R2: assert 0 == 1)
(c) spec passes through every creation and readiness path unchanged unchanged 609 passed, tests/unit/event_bus/
(d) list/diff before CreateTopics unchanged unchanged test_capacity_is_measured_once_per_provisioner
(e) ONEX_BOOT_UNIVERSE_PROVISION untouched unchanged unchanged —
(f) RED-before/GREEN-after against the real artifact the guard was the artifact and was not itself mutation-tested at module scope True. The guard is now mutation-tested against its own predecessor. M-R2 above

Gates — run on .200 (stickybeatz-studio), patch-transfer verified

Local commit → git format-patch → git am on the .200 worktree. Patch sha256 identical across the hop (4b62134f…), tree hash identical on both sides (65173cf48a8755736f3030bb1872c6e144776628), changed-file sha256 identical (23e66613…) — the gates ran on the same bytes that were pushed, and the push was issued from .200.

  • ruff format --check src/ tests/ — 4575 files already formatted
  • ruff check src/ tests/ — All checks passed
  • mypy src/omnibase_infra/ — Success, no issues in 2632 source files
  • governed selector (detect_test_paths.py) → {"selected_paths":["tests/unit/event_bus/"],"is_full_suite":false} — a single test file changed, no shared module touched, so the selector narrowed. No hand-typed -k.
  • pytest tests/unit/event_bus/ — 609 passed
  • pre-commit run --all-files

Honest state of pre-commit run --all-files — 3 failures, none from this diff:

  1. SPDX — tests/ci/test_runner_routing_audit.py and tests/scripts/test_deploy_runtime_core_contracts_resolution.py carry SPDX-FileCopyrightText: 2026 on origin/dev itself (verified: git show origin/dev:<path> | head -1 → 2026 for both). Neither file is in this diff. Same two offenders fix(OMN-15395): close the SECOND CreateTopics path — contract-driven RF on the operator CLI, capped drift, memoized probe, loud RF refusal #2552 recorded; still unfixed on dev.
  2. check-required-env-vars — wants GITHUB_TOKEN in .200's ~/.omnibase/.env. Machine env gap, not a repo defect.
  3. reject-required-check-skip-vector — ModuleNotFoundError: No module named 'yaml' from omniclaude/.github/actions/required-check-skip-guard/validate_no_required_check_skip_vectors.py. The script lives in a different repo and its interpreter on .200 lacks PyYAML. Environmental; new since fix(OMN-15395): close the SECOND CreateTopics path — contract-driven RF on the operator CLI, capped drift, memoized probe, loud RF refusal #2552's run.

pre-commit run --files <this PR's only changed file> → exit 0, every hook Passed. Commit-time hooks also ran clean (the commit was re-made after an initial -c core.hooksPath=.git/hooks invocation was caught as a silent bypass — in a worktree .git is a file, so that relative path resolves to nothing and skips every hook).

No skip tokens, no --no-verify, no -k narrowing.

Ticket: OMN-15395

Evidence-Ticket: OMN-15395
Evidence-Source: OCC#5551

…ot the module

The static guard shipped in #2552 computed admissibility once per FILE:

    resolves = ("ModelTopicProvisioningPolicy" in text
                and _POLICY_RESOLVER_RE.search(text))

so every NewTopic(...) in any module that mentions the policy anywhere got a
blanket pass unless its RF was an integer literal. Executed against the
reconstructed b2ca4fa (#2543) tree it reported only the operator CLI and
returned NOTHING for service_topic_manager.py:758's
`replication_factor=config.replication_factor` -- that lineage's own defect --
because the module mentions the policy five times elsewhere. That is the same
"the guard certified a property it could not see" failure the guard was
introduced to remediate, reintroduced in its replacement.

Admissibility is now computed from the NewTopic call's own argument expression
by AST provenance. A replication factor is admissible only when it traces, via
provenance-preserving operations (attribute, subscript, iteration, method call
on a resolved receiver, container literal/comprehension, single-arg builtin
repackaging), back to a resolve_spec / resolve_specs_for_creation /
resolve_replication_factor call. A module-local wrapper qualifies by its BODY
(every return resolved), never by its name, so a stub `_resolve_spec` that
returns its argument confers nothing. Name lookup is line-ordered per lexical
scope, which is what keeps managed_staging_topic_checker admissible: it binds
`spec` twice, once from a raw walrus and once from the resolved mapping, and
only the nearest preceding binding counts.

Also now refused rather than waved through: a literal RF in positional slot 3
(NewTopic('t', 6, 1)), an omitted RF, and a **kwargs splat.

Analysis is deliberately conservative: provenance is not tracked through a
mutated accumulator. That shape is reported, and the documented remedy is the
batch helper resolve_specs_for_creation -- not a loosening. Pinned by a test so
it stays a decision rather than a surprise.

Test-only change; no src/ or scripts/ behaviour is modified.

Ticket: OMN-15395

Evidence-Ticket: OMN-15395
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6aeef562-0ab6-4df5-b075-d70fd039ca8b

📥 Commits

Reviewing files that changed from the base of the PR and between f420d5f and 0bb3f09.

📒 Files selected for processing (1)
  • tests/unit/event_bus/test_topic_provisioner_rf_policy_omn15395.py

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

jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Jul 30, 2026
jonahgabriel added a commit to OmniNode-ai/onex_change_control that referenced this pull request Jul 30, 2026
jonahgabriel added a commit to OmniNode-ai/onex_change_control that referenced this pull request Jul 30, 2026
@jonahgabriel
jonahgabriel merged commit 3c78009 into dev Jul 30, 2026
129 of 138 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-15395-r3-createtopics-guard-ast branch July 30, 2026 03:48
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