Bind C-25 JSON to the scanned source commit (STATE-WP-0094-T03)
The checker JSON reports per repository repo_slug, source_revision (full 40-hex HEAD SHA) and source_revision_reason; C-25 findings repeat them so a findings-only consumer can bind. A dirty workplans/ tree, a non-git path, a missing HEAD or a tree changing mid-scan yields null plus a reason, never HEAD or a branch name. plan_owner_returns validates and passes the revision into every candidate and refuses findings bound elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 692292@bnt-lap001 Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
This commit is contained in:
parent
a0083ab78c
commit
150cb0b201
6 changed files with 284 additions and 1 deletions
|
|
@ -312,6 +312,10 @@ class ConsistencyReport:
|
|||
repo_path: str
|
||||
issues: list[Issue] = field(default_factory=list)
|
||||
fixes_applied: list[str] = field(default_factory=list)
|
||||
# Commit the workplans/ scan read (STATE-WP-0094): a full 40-hex SHA, or
|
||||
# None with a machine-readable reason. Never a branch name or a guess.
|
||||
source_revision: str | None = None
|
||||
source_revision_reason: str | None = "not_scanned"
|
||||
|
||||
def add(self, **kwargs) -> Issue:
|
||||
issue = Issue(**kwargs)
|
||||
|
|
@ -1465,6 +1469,9 @@ def check_repo(
|
|||
_check_orphan_db(api_base, repo_id, set(), report)
|
||||
return report
|
||||
|
||||
# Bind the scan to the commit it reads; re-verified after C-25 below.
|
||||
report.source_revision, report.source_revision_reason = scan_source_revision(repo_dir)
|
||||
|
||||
# Parse workplan files
|
||||
workplan_infos: list[tuple[Path, dict, str]] = []
|
||||
file_ws_ids: dict[str, tuple[Path, dict, str]] = {} # ws_id → (file, meta, body)
|
||||
|
|
@ -2429,6 +2436,10 @@ def check_repo(
|
|||
_check_ghost_duplicates(api_base, workplan_infos, file_ws_ids, report)
|
||||
|
||||
_check_blocked_unblock_sweep(api_base, repo_slug, repo_dir, workplan_infos, report)
|
||||
if report.source_revision is not None:
|
||||
# The tree must not have moved while the files were read and C-25 ran.
|
||||
if scan_source_revision(repo_dir) != (report.source_revision, None):
|
||||
report.source_revision, report.source_revision_reason = None, "changed_during_scan"
|
||||
_check_inbox_hygiene(api_base, repo_slug, report)
|
||||
_check_workplan_prefixes(repo_dir, repo_slug, workplan_infos, report)
|
||||
_check_workplan_id_collisions(api_base, repo_slug, repo_dir, workplan_infos, report)
|
||||
|
|
@ -2786,6 +2797,44 @@ def collect_inbox_hygiene(api_base: str, repo_slug: str) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
_FULL_SHA = re.compile(r"^[0-9a-f]{40}$")
|
||||
|
||||
|
||||
def scan_source_revision(repo_dir: Path) -> tuple[str | None, str | None]:
|
||||
"""Return ``(sha, None)`` for a clean ``workplans/`` tree, else ``(None, reason)``.
|
||||
|
||||
``sha`` is the full 40-hex HEAD commit. Any change under ``workplans/``
|
||||
(modified, staged, untracked or ignored) means the scanned files are not
|
||||
that commit, so no revision is claimed. Reasons: ``not_a_git_tree``,
|
||||
``no_head_commit``, ``unsupported_object_format``, ``dirty_workplans``,
|
||||
``git_status_failed``.
|
||||
"""
|
||||
def git(*args: str) -> str | None:
|
||||
try:
|
||||
return subprocess.run(
|
||||
["git", *args], cwd=repo_dir, capture_output=True, text=True,
|
||||
timeout=30, check=True,
|
||||
).stdout
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
|
||||
return None
|
||||
|
||||
if not Path(repo_dir).is_dir() or git("rev-parse", "--is-inside-work-tree") is None:
|
||||
return None, "not_a_git_tree"
|
||||
head = git("rev-parse", "--verify", "--quiet", "HEAD^{commit}")
|
||||
if head is None or not head.strip():
|
||||
return None, "no_head_commit"
|
||||
sha = head.strip()
|
||||
if not _FULL_SHA.fullmatch(sha):
|
||||
return None, "unsupported_object_format"
|
||||
status = git("status", "--porcelain", "--untracked-files=all", "--ignored",
|
||||
"--", "workplans")
|
||||
if status is None:
|
||||
return None, "git_status_failed"
|
||||
if status.strip():
|
||||
return None, "dirty_workplans"
|
||||
return sha, None
|
||||
|
||||
|
||||
C25_PAGE_SIZE = 200
|
||||
C25_MAX_PAGES = 50
|
||||
|
||||
|
|
@ -4360,9 +4409,14 @@ def render_text(report: ConsistencyReport, show_info: bool = True) -> str:
|
|||
|
||||
|
||||
def report_to_dict(report: ConsistencyReport) -> dict:
|
||||
revision = {
|
||||
"source_revision": report.source_revision,
|
||||
"source_revision_reason": report.source_revision_reason,
|
||||
}
|
||||
return {
|
||||
"repo_slug": report.repo_slug,
|
||||
"repo_path": report.repo_path,
|
||||
**revision,
|
||||
"issues": [
|
||||
{
|
||||
"severity": i.severity,
|
||||
|
|
@ -4373,6 +4427,7 @@ def report_to_dict(report: ConsistencyReport) -> dict:
|
|||
"file_value": i.file_value,
|
||||
"db_value": i.db_value,
|
||||
"fixable": i.fixable,
|
||||
**({"repo_slug": report.repo_slug, **revision} if i.check_id == "C-25" else {}),
|
||||
**({"owner_return": i._fix_context} if i.check_id == "C-25" and i._fix_context.get("task_record_id") else {}),
|
||||
}
|
||||
for i in report.issues
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue