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
2222Input 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+
273380def 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