fix: resolve conflicts for issue 330

Merge prgs/master into feat/issue-330-proof-wording-enforcement and keep
both assess_proof_wording (#330) and git ref mutation reporting (#297).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-07 05:16:57 -04:00
co-authored by Claude Opus 4.8
14 changed files with 1441 additions and 122 deletions
+402 -3
View File
@@ -968,6 +968,90 @@ def assess_mutation_ledger_report(
}
def assess_list_prs_pagination_proof(
list_prs_response: dict | list | None,
*,
inventory_complete_claimed: bool = False,
) -> dict:
"""#340: verify ``gitea_list_prs`` pagination metadata proves inventory scope.
Accepts the wrapped ``{"prs": [...], "pagination": {...}}`` response from
``gitea_list_prs``. Legacy bare lists fail closed when completeness is
claimed. Single-page fetches may not claim full inventory unless
``inventory_complete`` or ``is_final_page`` is true.
"""
reasons: list[str] = []
if list_prs_response is None:
reasons.append("gitea_list_prs response missing; fail closed")
return {"proven": False, "block": True, "reasons": reasons, "pagination": None}
if isinstance(list_prs_response, list):
pagination = None
if inventory_complete_claimed:
reasons.append(
"bare PR list lacks pagination metadata; cannot prove inventory "
"completeness"
)
return {
"proven": not reasons,
"block": bool(reasons),
"reasons": reasons,
"pagination": pagination,
}
if not isinstance(list_prs_response, dict):
reasons.append("gitea_list_prs response is not a dict or list; fail closed")
return {"proven": False, "block": True, "reasons": reasons, "pagination": None}
prs = list_prs_response.get("prs")
pagination = list_prs_response.get("pagination")
if not isinstance(prs, list):
reasons.append("gitea_list_prs response missing 'prs' list")
if not isinstance(pagination, dict):
reasons.append("gitea_list_prs response missing 'pagination' metadata")
return {
"proven": False,
"block": True,
"reasons": reasons,
"pagination": pagination if isinstance(pagination, dict) else None,
}
required_keys = ("page", "per_page", "returned_count", "has_more", "is_final_page")
for key in required_keys:
if key not in pagination:
reasons.append(f"pagination metadata missing '{key}'")
if inventory_complete_claimed:
complete = pagination.get("inventory_complete") is True
final_page = pagination.get("is_final_page") is True and pagination.get("has_more") is False
if not (complete or final_page):
reasons.append(
"inventory completeness claimed but pagination metadata does not "
"prove final page (inventory_complete or is_final_page with "
"has_more=false required)"
)
proven = not reasons
return {
"proven": proven,
"block": not proven,
"reasons": reasons,
"pagination": pagination,
}
def _prs_from_list_response(list_prs_response: list | dict | None) -> list | None:
"""Normalize ``gitea_list_prs`` output to a PR list."""
if list_prs_response is None:
return None
if isinstance(list_prs_response, list):
return list_prs_response
if isinstance(list_prs_response, dict):
prs = list_prs_response.get("prs")
return prs if isinstance(prs, list) else None
return None
def assess_role_boundary(proof=None, *, task_role=None, namespaces_used=None,
justification=None):
"""Assess reviewer/author role separation for blind queue workflows.
@@ -1583,8 +1667,55 @@ HANDOFF_ROLE_FIELDS = {
)),
("Related issues", ("related issues",)),
),
"create_issue": (
("Active profile", ("active profile",)),
("Runtime context", ("runtime context",)),
("Requested issue task", ("requested issue task",)),
("Workflow source", ("workflow source",)),
("Capability proof", ("capability proof",)),
("Duplicate search terms", ("duplicate search terms",)),
("Duplicate search pagination proof", (
"duplicate search pagination proof",
)),
("Duplicates found", ("duplicates found",)),
("Issues created", ("issues created",)),
("Issues commented", ("issues commented",)),
("Issues edited", ("issues edited",)),
("Issues skipped as duplicates", (
"issues skipped as duplicates",
)),
("File edits by issue creator", ("file edits by issue creator",)),
("Safety statement", ("safety statement",)),
),
}
CREATE_ISSUE_WORKFLOW_MARKERS = (
"workflows/create-issue.md",
"create-issue.md",
)
CREATE_ISSUE_TASK_MARKERS = (
"create-issue",
"create issue",
)
CREATE_ISSUE_FORBIDDEN_CROSS_MODE_FIELDS = (
("Selected issue", ("selected issue",)),
("Branch name", ("branch name",)),
("PR number", ("pr number",)),
("PR URL", ("pr url",)),
("Commit SHA", ("commit sha",)),
("Push result", ("push result",)),
("Baseline comparison", ("baseline comparison",)),
("Selected PR", ("selected pr",)),
("Review decision", ("review decision",)),
("Merge result", ("merge result",)),
("Merge preflight", ("merge preflight",)),
("Already-landed gate", ("already-landed gate",)),
("Pinned reviewed head", ("pinned reviewed head",)),
("Terminal review mutation", ("terminal review mutation",)),
)
def _handoff_section_lines(report_text):
"""Return the lines of the Controller Handoff section, or None."""
@@ -1635,6 +1766,11 @@ def assess_controller_handoff(report_text, role=None, local_edits=False):
fields_dict[label] = v.strip()
required = list(HANDOFF_BASE_FIELDS)
if role == "create_issue":
required = [
field for field in required
if field[0] not in ("Workspace mutations", "Mutations")
]
required.extend(HANDOFF_ROLE_FIELDS.get(role or "", ()))
missing = []
@@ -1822,14 +1958,19 @@ def pr_inventory_trust_gate(
"queue_target_lock": lock_status,
}
if list_prs_response is None or not isinstance(list_prs_response, list):
prs_list = _prs_from_list_response(list_prs_response)
pagination_meta = (
list_prs_response.get("pagination")
if isinstance(list_prs_response, dict) else {}
) or {}
if prs_list is None:
return {
"status": "inventory_error",
"reasons": ["PR list response is invalid (not a list or None)"],
"reasons": ["PR list response is invalid (missing prs list or None)"],
"corroborated": False,
}
if len(list_prs_response) > 0:
if len(prs_list) > 0:
return {
"status": "trusted_nonempty",
"reasons": [],
@@ -1860,6 +2001,13 @@ def pr_inventory_trust_gate(
corroborated = False
if has_finality_metadata:
corroborated = True
elif pagination_meta.get("inventory_complete") is True:
corroborated = True
elif (
pagination_meta.get("is_final_page") is True
and pagination_meta.get("has_more") is False
):
corroborated = True
elif corroboration_open_pr_counter == 0:
corroborated = True
else:
@@ -2659,6 +2807,141 @@ def assess_issue_filing_duplicate_summary(
}
def assess_create_issue_workflow_source(report_text: str) -> dict:
"""#337: create-issue reports must cite the canonical workflow file."""
lower = (report_text or "").lower()
reasons = []
if not any(marker in lower for marker in CREATE_ISSUE_WORKFLOW_MARKERS):
reasons.append(
"create-issue report missing workflow source "
"(workflows/create-issue.md)"
)
if not any(marker in lower for marker in CREATE_ISSUE_TASK_MARKERS):
reasons.append(
"create-issue report missing task mode declaration "
"(create-issue)"
)
return {
"complete": not reasons,
"downgraded": bool(reasons),
"reasons": reasons,
}
def assess_create_issue_mode_isolation(report_text: str) -> dict:
"""#337: reject work-issue/review-merge handoff fields in create-issue runs."""
section = _handoff_section_lines(report_text)
reasons = []
if section is None:
return {
"complete": True,
"downgraded": False,
"reasons": [],
}
labels = []
for line in section:
stripped = line.strip().lstrip("-*").strip()
if ":" in stripped:
labels.append(stripped.split(":", 1)[0].strip().lower())
for name, aliases in CREATE_ISSUE_FORBIDDEN_CROSS_MODE_FIELDS:
if any(
label.startswith(alias)
for label in labels
for alias in aliases
):
reasons.append(
f"create-issue handoff must not include cross-mode field "
f"'{name}'"
)
legacy_workspace = any(
label.startswith("workspace mutations") for label in labels
)
precise_categories = any(
label.startswith("file edits by issue creator")
for label in labels
)
if legacy_workspace and not precise_categories:
reasons.append(
"create-issue handoff must use precise mutation categories "
"instead of legacy 'Workspace mutations'"
)
return {
"complete": not reasons,
"downgraded": bool(reasons),
"reasons": reasons,
}
def assess_create_issue_final_report(
report_text: str,
*,
issue_number: int | None = None,
issue_title: str | None = None,
action: str = "created",
mutations: list[str] | None = None,
resolved_capabilities: dict | None = None,
issues_searched: int | None = None,
closest_matches: list[dict] | None = None,
duplicate_result: dict | None = None,
performed_mutations: list[str] | None = None,
) -> dict:
"""#337: composite verifier for create-issue final reports."""
checks = {
"workflow_source": assess_create_issue_workflow_source(report_text),
"mode_isolation": assess_create_issue_mode_isolation(report_text),
"controller_handoff": assess_controller_handoff(
report_text, role="create_issue"
),
"issue_reference": assess_issue_filing_issue_reference(
report_text,
issue_number=issue_number,
issue_title=issue_title,
action=action,
),
"mutation_capability": assess_issue_filing_mutation_capability(
report_text,
mutations or performed_mutations,
resolved_capabilities,
),
"mutation_scope": assess_issue_filing_mutation_scope(
report_text,
performed_mutations=performed_mutations or mutations,
),
"duplicate_summary": assess_issue_filing_duplicate_summary(
report_text,
issues_searched=issues_searched,
closest_matches=closest_matches,
duplicate_result=duplicate_result,
),
}
reasons = []
downgraded = False
for name, result in checks.items():
verdict = result.get("verdict")
if verdict in ("missing", "incomplete"):
downgraded = True
reasons.extend(result.get("reasons") or [])
elif result.get("downgraded") or not result.get("complete", True):
downgraded = True
reasons.extend(
f"{name}: {r}" for r in (result.get("reasons") or [])
)
grade = "A" if not downgraded else "downgraded"
return {
"grade": grade,
"downgraded": downgraded,
"checks": checks,
"reasons": reasons,
"complete": not downgraded,
}
def assess_issue_filing_final_report(
report_text: str,
*,
@@ -2952,3 +3235,119 @@ def assess_proof_wording(
"flagged_rules": flagged_rules,
"reasons": violations,
}
# ---------------------------------------------------------------------------
# Git ref mutations (#297)
# ---------------------------------------------------------------------------
_GIT_REF_MUTATIONS_FIELD_RE = re.compile(
r"^\s*git ref mutations\s*:\s*(.+?)\s*$", re.I | re.M)
_GIT_WORKTREE_MUTATIONS_NONE_RE = re.compile(
r"^\s*git/worktree mutations\s*:\s*none\b", re.I | re.M)
_GIT_REF_MUTATING_FIRST_WORDS = frozenset(
{"fetch", "pull", "push", "merge", "update-ref"})
_GIT_REF_MUTATING_PREFIXES = ("remote update", "branch -d", "branch -D",
"reset --hard")
def _command_text(entry):
if isinstance(entry, dict):
return str(entry.get("command") or "").strip()
return str(entry or "").strip()
def _git_subcommand_line(command):
tokens = command.split()
if not tokens or tokens[0] != "git":
return None
return " ".join(tokens[1:])
def git_ref_mutating_commands(command_log):
"""Return logged commands that update git refs (#297).
``git fetch`` and friends edit no files but move refs; they are never
read-only diagnostics.
"""
out = []
for entry in command_log or []:
command = _command_text(entry)
rest = _git_subcommand_line(command)
if rest is None:
continue
first = rest.split()[0] if rest.split() else ""
if first in _GIT_REF_MUTATING_FIRST_WORDS:
out.append(command)
continue
if any(rest.startswith(prefix) for prefix in _GIT_REF_MUTATING_PREFIXES):
out.append(command)
return out
def _fetch_targets(ref_commands):
"""Extract ``<remote>/<branch>`` targets from git fetch commands."""
targets = []
for command in ref_commands:
rest = _git_subcommand_line(command) or ""
tokens = rest.split()
if not tokens or tokens[0] != "fetch":
continue
args = [t for t in tokens[1:] if not t.startswith("-")]
if len(args) >= 2:
targets.append(f"{args[0]}/{args[1]}")
return targets
def assess_git_ref_mutation_report(report_text, *, command_log=None):
"""#297: ref updates are Git ref mutations, not read-only diagnostics.
When the command log holds ref-updating commands, the report must carry
a non-none ``Git ref mutations`` entry (naming ``fetched
<remote>/<branch>`` for targeted fetches), may not claim ``Git/worktree
mutations: None``, and may not list the command as read-only.
"""
text = report_text or ""
lower = text.lower()
reasons = []
ref_commands = git_ref_mutating_commands(command_log)
fetch_targets = _fetch_targets(ref_commands)
if ref_commands:
field = _GIT_REF_MUTATIONS_FIELD_RE.search(text)
value = field.group(1).strip().lower() if field else None
if not value or value == "none":
reasons.append(
"ref-updating commands ran but the report has no "
"'Git ref mutations' entry"
)
for target in fetch_targets:
if "fetched" not in lower or target.lower() not in lower:
reasons.append(
"git fetch ran; report must state "
f"'Git ref mutations: fetched {target}'"
)
if _GIT_WORKTREE_MUTATIONS_NONE_RE.search(text):
reasons.append(
"'Git/worktree mutations: None' rejected: ref-updating "
"commands ran"
)
for line in text.splitlines():
if "read-only" not in line.lower():
continue
for command in ref_commands:
if command.lower() in line.lower():
reasons.append(
"read-only diagnostics may not include ref-updating "
f"command '{command}'"
)
proven = not reasons
return {
"proven": proven,
"block": not proven,
"reasons": reasons,
"ref_mutating_commands": ref_commands,
"fetch_targets": fetch_targets,
}