Skip to content

Commit 4216bb5

Browse files
rjmurillo-botclaude
andcommitted
fix(ci): resolve merge with proper dual-mode validation and type safety
Fix mypy type error (variable shadowing ClaimViolation with str), guard against bool values in session JSON fields, extract validation modes into separate functions, and default to diff mode for backward compatibility with existing tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3d549e1 commit 4216bb5

File tree

1 file changed

+114
-93
lines changed

1 file changed

+114
-93
lines changed

.github/scripts/validate_investigation_claims.py

Lines changed: 114 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
--commits: Comma-separated commit SHAs to validate (optional)
1818
--base-ref: Base ref for diff comparison (default: HEAD~1)
1919
--head-ref: Head ref for diff comparison (default: HEAD)
20-
--mode: Validation mode: session (default), diff, or both
20+
--mode: Validation mode: session, diff (default), or both
2121
2222
Input env vars (used as defaults for CLI args):
2323
GITHUB_OUTPUT - Path to GitHub Actions output file
@@ -127,6 +127,9 @@ def session_claims_investigation_only(session_path: Path) -> bool:
127127
# Look for qaValidation or related fields with SKIPPED evidence
128128
for key in ("qaValidation", "checklistComplete"):
129129
item = session_end.get(key, {})
130+
# Guard against non-dict values (e.g., booleans)
131+
if not isinstance(item, dict):
132+
continue
130133
evidence = item.get("evidence", "")
131134
if _INVESTIGATION_CLAIM_PATTERN.search(str(evidence)):
132135
return True
@@ -264,12 +267,116 @@ def build_parser() -> argparse.ArgumentParser:
264267
parser.add_argument(
265268
"--mode",
266269
choices=["session", "diff", "both"],
267-
default="both",
268-
help="Validation mode: session (session-log), diff (git-diff), both (default)",
270+
default="diff",
271+
help="Validation mode: session (session-log), diff (git-diff), both",
269272
)
270273
return parser
271274

272275

276+
def _run_session_validation(args: argparse.Namespace) -> int:
277+
"""Run session-log-based validation. Returns exit code."""
278+
if not args.session_dir.exists():
279+
write_log(f"Session directory not found: {args.session_dir}")
280+
print(f"ERROR: Session directory not found: {args.session_dir}")
281+
return 1
282+
283+
commits = [c.strip() for c in args.commits.split(",") if c.strip()] or None
284+
285+
result = validate_investigation_claims(args.session_dir, commits)
286+
287+
# Write GitHub Actions outputs
288+
write_output("verdict", "PASS" if result.valid else "FAIL")
289+
write_output("violation_count", str(len(result.violations)))
290+
291+
if result.violations:
292+
violations_json = json.dumps(
293+
[
294+
{
295+
"session": v.session_file,
296+
"commit": v.commit_sha,
297+
"files": v.disallowed_files,
298+
}
299+
for v in result.violations
300+
]
301+
)
302+
write_output("violations", violations_json)
303+
304+
# Output report
305+
if args.output_format == "json":
306+
report = {
307+
"valid": result.valid,
308+
"sessions_checked": result.sessions_checked,
309+
"claims_found": result.claims_found,
310+
"violations": [
311+
{
312+
"session_file": v.session_file,
313+
"commit_sha": v.commit_sha,
314+
"disallowed_files": v.disallowed_files,
315+
}
316+
for v in result.violations
317+
],
318+
}
319+
print(json.dumps(report, indent=2))
320+
else:
321+
print("Investigation-Only Claim Validation")
322+
print(f"Sessions checked: {result.sessions_checked}")
323+
print(f"Claims found: {result.claims_found}")
324+
print(f"Violations: {len(result.violations)}")
325+
326+
if result.violations:
327+
print()
328+
print("VIOLATIONS DETECTED:")
329+
for violation in result.violations:
330+
print(
331+
f" Session: {violation.session_file} "
332+
f"(commit {violation.commit_sha})"
333+
)
334+
for filepath in violation.disallowed_files:
335+
print(f" - {filepath}")
336+
337+
return 0 if result.valid else 1
338+
339+
340+
def _run_diff_validation(args: argparse.Namespace) -> int:
341+
"""Run git-diff-based validation. Returns exit code (advisory, always 0)."""
342+
changed_files = get_changed_files(args.base_ref, args.head_ref)
343+
if not changed_files:
344+
write_log("No changed files found.")
345+
write_output("investigation_violations", "0")
346+
write_output("violation_details", "")
347+
return 0
348+
349+
write_log(f"Checking {len(changed_files)} changed file(s) against allowlist")
350+
351+
violations = validate_claims(changed_files)
352+
353+
write_output("investigation_violations", str(len(violations)))
354+
355+
if violations:
356+
details = "\n".join(f" - {filepath}" for filepath in violations)
357+
write_output("violation_details", details)
358+
write_log(
359+
f"Investigation-only claim violated by {len(violations)} file(s):"
360+
)
361+
for filepath in violations:
362+
write_log(f" {filepath}")
363+
write_log("")
364+
write_log("Allowed paths:")
365+
for path in get_investigation_allowlist_display():
366+
write_log(f" {path}")
367+
print(
368+
f"::warning::Investigation-only claim violated by "
369+
f"{len(violations)} file(s). This is advisory only.",
370+
file=sys.stderr,
371+
)
372+
else:
373+
write_output("violation_details", "")
374+
write_log("All changed files match investigation-only allowlist.")
375+
376+
# Advisory only: always exit 0
377+
return 0
378+
379+
273380
def main(argv: list[str] | None = None) -> int:
274381
"""Main entry point."""
275382
args = build_parser().parse_args(argv)
@@ -286,99 +393,13 @@ def main(argv: list[str] | None = None) -> int:
286393

287394
# Session-log mode: validate session files claiming investigation-only
288395
if args.mode in ("session", "both"):
289-
if not args.session_dir.exists():
290-
write_log(f"Session directory not found: {args.session_dir}")
291-
print(f"ERROR: Session directory not found: {args.session_dir}")
292-
return 1
293-
294-
commits = [c.strip() for c in args.commits.split(",") if c.strip()] or None
295-
296-
result = validate_investigation_claims(args.session_dir, commits)
297-
298-
# Write GitHub Actions outputs
299-
write_output("verdict", "PASS" if result.valid else "FAIL")
300-
write_output("violation_count", str(len(result.violations)))
301-
302-
if result.violations:
303-
violations_json = json.dumps(
304-
[
305-
{
306-
"session": v.session_file,
307-
"commit": v.commit_sha,
308-
"files": v.disallowed_files,
309-
}
310-
for v in result.violations
311-
]
312-
)
313-
write_output("violations", violations_json)
314-
315-
# Output report
316-
if args.output_format == "json":
317-
report = {
318-
"valid": result.valid,
319-
"sessions_checked": result.sessions_checked,
320-
"claims_found": result.claims_found,
321-
"violations": [
322-
{
323-
"session_file": v.session_file,
324-
"commit_sha": v.commit_sha,
325-
"disallowed_files": v.disallowed_files,
326-
}
327-
for v in result.violations
328-
],
329-
}
330-
print(json.dumps(report, indent=2))
331-
else:
332-
print("Investigation-Only Claim Validation")
333-
print(f"Sessions checked: {result.sessions_checked}")
334-
print(f"Claims found: {result.claims_found}")
335-
print(f"Violations: {len(result.violations)}")
336-
337-
if result.violations:
338-
print()
339-
print("VIOLATIONS DETECTED:")
340-
for v in result.violations:
341-
print(f" Session: {v.session_file} (commit {v.commit_sha})")
342-
for f in v.disallowed_files:
343-
print(f" - {f}")
344-
345-
if not result.valid:
346-
exit_code = 1
396+
exit_code = _run_session_validation(args)
347397

348398
# Diff mode: validate changed files against shared allowlist
349399
if args.mode in ("diff", "both"):
350-
changed_files = get_changed_files(args.base_ref, args.head_ref)
351-
if not changed_files:
352-
write_log("No changed files found.")
353-
write_output("investigation_violations", "0")
354-
write_output("violation_details", "")
355-
else:
356-
write_log(f"Checking {len(changed_files)} changed file(s) against allowlist")
357-
358-
violations = validate_claims(changed_files)
359-
360-
write_output("investigation_violations", str(len(violations)))
361-
362-
if violations:
363-
details = "\n".join(f" - {v}" for v in violations)
364-
write_output("violation_details", details)
365-
write_log(
366-
f"Investigation-only claim violated by {len(violations)} file(s):"
367-
)
368-
for v in violations:
369-
write_log(f" {v}")
370-
write_log("")
371-
write_log("Allowed paths:")
372-
for path in get_investigation_allowlist_display():
373-
write_log(f" {path}")
374-
print(
375-
f"::warning::Investigation-only claim violated by "
376-
f"{len(violations)} file(s). This is advisory only.",
377-
file=sys.stderr,
378-
)
379-
else:
380-
write_output("violation_details", "")
381-
write_log("All changed files match investigation-only allowlist.")
400+
diff_code = _run_diff_validation(args)
401+
if diff_code != 0:
402+
exit_code = diff_code
382403

383404
return exit_code
384405

0 commit comments

Comments
 (0)