2727from pathlib import Path
2828import re
2929import shutil
30+ import subprocess
3031import tempfile
3132
3233
@@ -432,7 +433,59 @@ def validate_invariants(sdd: str, errors: list[str]) -> int:
432433 return len (ids )
433434
434435
435- def validate_artifacts (root : Path , sdd : str , errors : list [str ]) -> None :
436+ def git_changed_paths (root : Path , baseline_ref : str , errors : list [str ]) -> set [str ]:
437+ object_check = subprocess .run (
438+ ["git" , "-C" , str (root ), "cat-file" , "-e" , f"{ baseline_ref } ^{{commit}}" ],
439+ capture_output = True ,
440+ text = True ,
441+ encoding = "utf-8" ,
442+ check = False ,
443+ )
444+ if object_check .returncode != 0 :
445+ errors .append (f"baseline_ref is not available as a Git commit: { baseline_ref } " )
446+ return set ()
447+
448+ diff = subprocess .run (
449+ ["git" , "-C" , str (root ), "diff" , "--name-only" , baseline_ref , "--" ],
450+ capture_output = True ,
451+ text = True ,
452+ encoding = "utf-8" ,
453+ check = False ,
454+ )
455+ whitespace = subprocess .run (
456+ ["git" , "-C" , str (root ), "diff" , "--check" , baseline_ref , "--" ],
457+ capture_output = True ,
458+ text = True ,
459+ encoding = "utf-8" ,
460+ check = False ,
461+ )
462+ untracked = subprocess .run (
463+ ["git" , "-C" , str (root ), "ls-files" , "--others" , "--exclude-standard" ],
464+ capture_output = True ,
465+ text = True ,
466+ encoding = "utf-8" ,
467+ check = False ,
468+ )
469+ if whitespace .returncode != 0 :
470+ details = (whitespace .stdout + whitespace .stderr ).strip ()
471+ errors .append (f"WP0 committed/working diff has whitespace errors: { details } " )
472+ if diff .returncode != 0 or untracked .returncode != 0 :
473+ errors .append ("unable to compute WP0 changed paths from baseline_ref" )
474+ return set ()
475+ return {
476+ path .strip ().replace ("\\ " , "/" )
477+ for path in (diff .stdout + "\n " + untracked .stdout ).splitlines ()
478+ if path .strip ()
479+ }
480+
481+
482+ def validate_artifacts (
483+ root : Path ,
484+ sdd : str ,
485+ fields : dict [str , str ],
486+ errors : list [str ],
487+ check_git_diff : bool ,
488+ ) -> None :
436489 expected = set (EXPECTED_WP0_ARTIFACTS )
437490 wp0 = work_package_blocks (sdd ).get ("WP0" , "" )
438491 scope_match = re .search (
@@ -474,6 +527,14 @@ def validate_artifacts(root: Path, sdd: str, errors: list[str]) -> None:
474527 if len (text .splitlines ()) > 20 :
475528 errors .append (f"legacy pointer must not maintain an independent state copy: { relative } " )
476529
530+ if check_git_diff and fields .get ("current_work_package" ) == "WP0" :
531+ changed = git_changed_paths (root , fields .get ("baseline_ref" , "" ), errors )
532+ if changed != expected :
533+ errors .append (
534+ "WP0 changed paths differ from the expected artifact registry: "
535+ f"missing={ sorted (expected - changed )} , unexpected={ sorted (changed - expected )} "
536+ )
537+
477538
478539def validate_markdown (
479540 root : Path ,
@@ -549,6 +610,7 @@ def validate_governance_terms(root: Path, errors: list[str]) -> None:
549610def validate (
550611 root : Path ,
551612 * ,
613+ check_git_diff : bool = True ,
552614 check_markdown : bool = True ,
553615 markdown_paths : tuple [str , ...] | None = None ,
554616) -> tuple [list [str ], dict [str , object ]]:
@@ -567,7 +629,7 @@ def validate(
567629 validate_current_state (sdd , fields , errors )
568630 validate_wp0_gate_contract (sdd , errors )
569631 invariant_count = validate_invariants (sdd , errors )
570- validate_artifacts (root , sdd , errors )
632+ validate_artifacts (root , sdd , fields , errors , check_git_diff )
571633 if check_markdown :
572634 validate_markdown (root , errors , markdown_paths )
573635 validate_governance_terms (root , errors )
@@ -611,6 +673,7 @@ def expect_failure(
611673 mutation (candidate )
612674 errors , _ = validate (
613675 candidate ,
676+ check_git_diff = False ,
614677 check_markdown = check_markdown ,
615678 markdown_paths = markdown_paths ,
616679 )
@@ -681,6 +744,7 @@ def add_deprecated_issue_keyword(candidate: Path) -> None:
681744 )
682745 prose_errors , _ = validate (
683746 candidate ,
747+ check_git_diff = False ,
684748 check_markdown = False ,
685749 )
686750 deprecated_errors = [
@@ -911,7 +975,7 @@ def remove_live_gate(candidate: Path) -> None:
911975 suffix = suffix .replace ("| Status | in-progress |" , "| Status | implemented |" , 1 )
912976 path .write_text (prefix + suffix , encoding = "utf-8" )
913977 lifecycle_errors , _ = validate (
914- candidate , check_markdown = False
978+ candidate , check_git_diff = False , check_markdown = False
915979 )
916980 if lifecycle_errors :
917981 raise AssertionError (f"implemented lifecycle state must remain valid: { lifecycle_errors } " )
0 commit comments