Skip to content

fix: add stable review contract identities#79

Open
Pigbibi wants to merge 2 commits into
mainfrom
codex/pr-review-contract-identity
Open

fix: add stable review contract identities#79
Pigbibi wants to merge 2 commits into
mainfrom
codex/pr-review-contract-identity

Conversation

@Pigbibi

@Pigbibi Pigbibi commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add stable contract_key and behavior_digest identities for review findings
  • make fingerprint v2 independent of severity-only drift
  • serialize versioned history v2 while reading legacy v1 safely
  • fail closed when stored identity digests do not recompute exactly
  • add sanitized fix: classify adapter dispatch outcomes #75 and #125 replay fixtures for wording drift, opposite behavior, and unrelated same-file contracts

This is C2 of the replacement sequence for #77. It does not change the convergence state machine or required check names.

Verification

  • targeted unittest: 58 passed
  • full unittest discover: 624 passed, 1 skipped
  • ruff check passed
  • compileall passed
  • actionlint passed
  • git diff check passed

Co-Authored-By: Codex <noreply@openai.com>
@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown

🤖 Codex PR Review

🚫 Merge blocked: 2 serious issue(s) found in high-risk files

⚖️ Codex Review Arbitration

🚫 block: Both current findings remain valid in the shown source. First, _contract_subject() still falls back to _normalize_contract_text(finding.get("suggestion") or text), so when no backtick/dotted-code anchors are found and suggestion is populated, distinct descriptions in the same file/category collapse to the same contract_key; nothing in the tests or schema disproves that case. Second, conflicting_contract_findings() still only marks prior behaviors resolved on clear/cleared rounds and otherwise accumulates all historical behavior_digest values per contract_key, so if history records behavior A and later accepts behavior B, the old A digest remains in prior_by_contract and future runs with B alone still emit contract_conflict. The prior blocking finding does not conflict with these: it required distinct same-contract findings to be matched independently, while the current findings identify remaining cases where that still fails or never converges.

🚫 Blocking Issues

These issues must be fixed before this PR can be merged:

1. 🟠 [HIGH] Logic in scripts/run_codex_pr_review.py

_contract_subject() drops the description entirely whenever suggestion is non-empty and no code anchors are found, because the fallback is _normalize_contract_text(finding.get("suggestion") or text). That makes two different findings in the same file/category with the same generic suggestion text produce the same contract_key, which can suppress distinct findings or fabricate history/conflict matches. (line 824)

Suggestion: Build the fallback subject from both description and suggestion instead of preferring suggestion alone, or hash them as separate fields so distinct findings cannot collapse just because they share generic remediation text.

2. 🟠 [HIGH] Logic in scripts/run_codex_pr_review.py

conflicting_contract_findings() only resolves older behaviors on clear/cleared rounds. If a contract flips from behavior A to behavior B and that new behavior is later recorded again (including after human arbitration), the old unresolved digest for A stays in prior_by_contract, so every subsequent run with B still reports contract_conflict. This creates a non-converging state where accepted behavior changes can block PRs indefinitely. (line 1369)

Suggestion: When scanning history, treat newer rounds for the same contract_key as superseding older behavior digests, or explicitly resolve prior digests when an arbitration/human-contract status records the accepted behavior for that contract.


Review by Codex PR Review bot • PR

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

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

if not isinstance(findings, list):
return [], False
if status not in {"blocking", "clear", "cleared", "overflow", "invalid_history"}:
if status not in {"blocking", "clear", "cleared", "overflow", "invalid_history", "initial_review", "remediation", "arbitration_required", "blocked_human_contract"}:

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 Treat accepted contract-block statuses as active

When a trusted history marker uses one of the newly accepted statuses such as arbitration_required or blocked_human_contract, parse_finding_history now returns valid history, but has_active_blocking_history() only treats blocking plus overflow/invalid sentinels as active. If the next primary review is clean, active_blocking_history stays false and the PR can be allowed and re-serialized as clear without the independent confirmation these statuses require. Either reject these statuses here or include the blocking ones in the active/confirmation checks.

Useful? React with 👍 / 👎.

Comment on lines +818 to +822
text = " ".join(
str(finding.get(field) or "") for field in ("description", "suggestion")
)
anchors = re.findall(r"`([^`]{1,120})`", text)
anchors.extend(re.findall(r"\b[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+\b", text))

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 Keep implementation anchors out of the contract key

Because the subject is built from backtick anchors in both description and suggestion, two findings for the same described contract stop sharing a contract_key as soon as their opposing suggestions name different code symbols. For example, prior `PanelContract` ... / Return ReviewResult(...)and current PanelContract ... / `Raise `ReviewError hash as different contracts, so conflicting_contract_findings() never compares their behavior and auto-remediation may proceed on contradictory instructions.

Useful? React with 👍 / 👎.

Comment thread scripts/run_codex_pr_review.py Outdated
}
prior_by_contract: dict[str, dict[str, Any]] = {}
for round_state in reversed(history):
if round_state.get("status") in {"clear", "cleared"} and not round_state.get("findings"):

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 Do not index cleared findings as active conflicts

A cleared round can contain the findings that independent arbitration already invalidated, but this loop only stops on clear/cleared rounds with no findings and otherwise indexes those cleared findings as prior active contracts. When a later review reports the same contract with different behavior, contract_conflict is set and auto_fix_allowed is disabled even though the earlier finding was resolved; cleared rounds with findings should be skipped or treated as resolving those contract keys, matching previous_matching_findings().

Useful? React with 👍 / 👎.

decision.update(
{
"blocked": True,
"contract_conflict": True,

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 Preserve contract conflicts through later arbitration

When a conflict detected here coexists with another repeated finding, the repeated finding can still trigger arbitration using only previous_findings, and apply_arbitration_result() later overwrites contract_conflict/auto_fix_allowed from that arbiter response. If the arbiter returns contract_conflict: false for the repeated finding, the conflict found in this block is lost and auto-remediation is re-enabled; include these conflict matches in arbitration or preserve the flag after arbitration.

Useful? React with 👍 / 👎.

Comment on lines +845 to +846
"contract_key": _contract_key(finding),
"behavior": _normalize_contract_text(finding.get("suggestion") or finding.get("description")),

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 Hash the described behavior, not only generic suggestions

For findings with a non-empty but generic suggestion, this ignores the description even when the description contains the actual required behavior. If two rounds share the same contract anchor and suggestion such as Update the guard, but the descriptions flip from “must reject” to “must accept”, the behavior digest stays identical, so the change is treated as a repeat instead of a contract conflict and auto-remediation can follow the contradictory current instruction.

Useful? React with 👍 / 👎.

if not isinstance(findings, list):
findings = []
findings = [
{**finding, **_contract_finding(finding)}

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 Preserve reviewer text when adding contract IDs

This merge overwrites the reviewer-provided description and suggestion with _contract_finding()'s history-sanitized versions before formatting the PR comment and decision artifact. For findings that mention normal code identifiers containing digits, such as fingerprint_v2 or HTTP2Settings, the history scrubber replaces those tokens with [REDACTED], so the human-facing review can lose the exact symbol the author needs to fix; keep sanitized copies only for the history marker/identity fields.

Useful? React with 👍 / 👎.



def _normalize_contract_text(value: Any) -> str:
text = _sanitize_history_text(value, 240).lower()

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 Do not redact versioned identifiers before hashing

Using the history scrubber before extracting contract tokens collapses common versioned identifiers to the same redacted token. In a same-file/category review, anchors such as TokenV2Contract and TokenV3Contract therefore produce the same subject, which can make unrelated version-specific findings repeat or conflict with each other and incorrectly disable auto-remediation.

Useful? React with 👍 / 👎.

Co-Authored-By: Codex <noreply@openai.com>

@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: 00eb36a9ed

ℹ️ 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 +2138 to +2140
contract_conflicts = conflicting_contract_findings(
finding_history, decision["blocking_findings"]
)

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 Feed detected contract conflicts into arbitration

When contract_conflicts is non-empty for the normal opposite-behavior case, previous_findings is often empty because matching now requires the same (contract_key, behavior_digest). This block sets next_action to contract_arbitration, but the later arbitration condition only looks at previous_findings/repeats, so the independent arbiter is never called and the prior conflicting finding is never sent with the diff; a false-positive conflict can only remain blocked instead of being cleared or confirmed by the arbiter.

Useful? React with 👍 / 👎.

Comment on lines +824 to +826
return "|".join(sorted(anchor for anchor in normalized if anchor)) or _normalize_contract_text(
finding.get("suggestion") or text
)

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 Keep fallback subjects independent of fixes

When a reviewer describes the same contract in prose without backticks or dotted symbols, this fallback uses the suggestion as the contract_key subject. Opposite findings such as prior “return a blocked result” and current “raise an error” then hash to different contracts, so conflicting_contract_findings() and previous_matching_findings() treat them as unrelated and leave auto-remediation enabled instead of requiring contract arbitration.

Useful? React with 👍 / 👎.

Comment on lines +1372 to +1374
if status in {"clear", "cleared"}:
resolved.add(identity)
continue

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 Ignore older clear rounds after a contract is re-blocked

Because this reverse scan records every cleared identity even after a newer blocking round for the same identity was already added to prior_by_contract, a history like blocked A → cleared A → blocked A will have the latest active A suppressed by the older clear. If the current finding then changes A's behavior, conflicting_contract_findings() returns no conflict and can leave the reintroduced blocker outside contract arbitration.

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