9797 re .compile (r"\b(?:tests?|specs?|fixtures?|scripts?)/[^\s,.;)]+" ),
9898 re .compile (r"`?\.github/workflows/[^\s,.;)`]+`?" ),
9999 re .compile (r"\b(?:npm|pnpm|yarn|bun)\s+run\s+[\w:./-]+" ),
100- re .compile (r"\b(?:make|just)\s+[\w:./-]+" ),
100+ re .compile (
101+ r"\bmake(?:\s+[\w.-]+=[^\s,;)`\]}]+)*\s+(?!-)[\w:./-]+"
102+ r"(?=$|[\s,.;)`\]}])"
103+ ),
104+ re .compile (r"\bjust\s+(?!-)[\w:./-]+" ),
101105 re .compile (r"\bpython3?\s+(?:-m\s+[\w.:-]+|scripts?/[^\s,.;)]+)" ),
102106 re .compile (r"\bpytest\s+(?:-[\w-]+|tests?/[^\s,.;)]+|[\w/.-]+)" ),
103107 re .compile (r"\b(?:vitest|jest|ruff|mypy|eslint)\s+[\w/.:@-]+" ),
116120PACKAGE_SCRIPT_COMMAND_RE = re .compile (
117121 r"\b(?P<manager>npm|pnpm|yarn|bun)\s+run\s+(?P<script>[\w:./-]+)"
118122)
123+ MAKE_COMMAND_RE = re .compile (
124+ r"\bmake(?:\s+[\w.-]+=[^\s,;)`\]}]+)*\s+(?!-)(?P<target>[\w:./-]+)"
125+ r"(?=$|[\s,.;)`\]}])"
126+ )
127+ JUST_COMMAND_RE = re .compile (r"\bjust\s+(?!-)(?P<recipe>[\w:./-]+)" )
128+ MAKEFILE_NAMES = ("GNUmakefile" , "makefile" , "Makefile" )
129+ JUSTFILE_NAMES = ("justfile" , "Justfile" , ".justfile" )
119130
120131FAILURE_RECORD_RE = re .compile (
121132 r"`?(docs/failures/[^\s,;)`]+)`?" ,
@@ -429,6 +440,10 @@ def normalize_package_script(value: str) -> str:
429440 return value .rstrip (".,;)]}" )
430441
431442
443+ def normalize_command_target (value : str ) -> str :
444+ return value .rstrip (".,;)]}" )
445+
446+
432447def root_package_scripts (root : Path ) -> set [str ]:
433448 package_json = root / "package.json"
434449 if not package_json .exists ():
@@ -443,6 +458,71 @@ def root_package_scripts(root: Path) -> set[str]:
443458 return {str (name ) for name in package_scripts }
444459
445460
461+ def root_make_targets (root : Path ) -> set [str ]:
462+ targets : set [str ] = set ()
463+ path = next (
464+ (root / name for name in MAKEFILE_NAMES if (root / name ).exists ()),
465+ None ,
466+ )
467+ if path is None :
468+ return targets
469+ try :
470+ lines = path .read_text (encoding = "utf-8" ).splitlines ()
471+ except (OSError , UnicodeDecodeError ):
472+ return targets
473+ for raw_line in lines :
474+ if not raw_line or raw_line [:1 ].isspace ():
475+ continue
476+ line = raw_line .split ("#" , 1 )[0 ].rstrip ()
477+ if ":" not in line :
478+ continue
479+ target_part , rule_part = line .split (":" , 1 )
480+ if not target_part .strip () or "=" in target_part :
481+ continue
482+ if rule_part .lstrip ().startswith ("=" ):
483+ continue
484+ for target in target_part .split ():
485+ if target and "%" not in target and not target .startswith ("." ):
486+ targets .add (target )
487+ return targets
488+
489+
490+ def root_just_recipes (root : Path ) -> set [str ]:
491+ recipes : set [str ] = set ()
492+ for name in JUSTFILE_NAMES :
493+ path = root / name
494+ if not path .exists ():
495+ continue
496+ try :
497+ lines = path .read_text (encoding = "utf-8" ).splitlines ()
498+ except (OSError , UnicodeDecodeError ):
499+ continue
500+ for raw_line in lines :
501+ if not raw_line or raw_line [:1 ].isspace ():
502+ continue
503+ line = raw_line .split ("#" , 1 )[0 ].rstrip ()
504+ alias_match = re .match (r"alias\s+(?P<name>[\w.-]+)\s*:=" , line )
505+ if alias_match is not None :
506+ recipes .add (alias_match .group ("name" ))
507+ continue
508+ if ":" not in line :
509+ continue
510+ recipe_part , rule_part = line .split (":" , 1 )
511+ if not recipe_part .strip ():
512+ continue
513+ if rule_part .lstrip ().startswith ("=" ):
514+ continue
515+ recipe_part = recipe_part .strip ()
516+ while recipe_part .startswith ("[" ) and "]" in recipe_part :
517+ recipe_part = recipe_part .split ("]" , 1 )[1 ].strip ()
518+ if not recipe_part :
519+ continue
520+ recipe = recipe_part .split ()[0 ].lstrip ("@" )
521+ if recipe and not recipe .startswith ("[" ):
522+ recipes .add (recipe )
523+ return recipes
524+
525+
446526def missing_package_script_commands (root : Path , value : str | None ) -> list [str ]:
447527 if value is None :
448528 return []
@@ -463,6 +543,38 @@ def missing_package_script_commands(root: Path, value: str | None) -> list[str]:
463543 ]
464544
465545
546+ def missing_make_commands (root : Path , value : str | None ) -> list [str ]:
547+ if value is None :
548+ return []
549+ commands = sorted (
550+ {
551+ normalize_command_target (match .group ("target" ))
552+ for match in MAKE_COMMAND_RE .finditer (value )
553+ }
554+ )
555+ if not commands :
556+ return []
557+
558+ targets = root_make_targets (root )
559+ return [f"make { target } " for target in commands if target not in targets ]
560+
561+
562+ def missing_just_commands (root : Path , value : str | None ) -> list [str ]:
563+ if value is None :
564+ return []
565+ commands = sorted (
566+ {
567+ normalize_command_target (match .group ("recipe" ))
568+ for match in JUST_COMMAND_RE .finditer (value )
569+ }
570+ )
571+ if not commands :
572+ return []
573+
574+ recipes = root_just_recipes (root )
575+ return [f"just { recipe } " for recipe in commands if recipe not in recipes ]
576+
577+
466578def says_no_failure_record (value : str | None ) -> bool :
467579 if value is None :
468580 return False
@@ -626,6 +738,26 @@ def validate_adoption_report(root: Path, path: Path, text: str) -> list[Finding]
626738 ),
627739 )
628740 )
741+ for command in missing_make_commands (root , detection_value ):
742+ findings .append (
743+ Finding (
744+ path ,
745+ (
746+ "failure-memory detection references missing "
747+ f"Makefile target: { command } "
748+ ),
749+ )
750+ )
751+ for command in missing_just_commands (root , detection_value ):
752+ findings .append (
753+ Finding (
754+ path ,
755+ (
756+ "failure-memory detection references missing "
757+ f"justfile recipe: { command } "
758+ ),
759+ )
760+ )
629761
630762 return findings
631763
@@ -646,7 +778,7 @@ def validate_effectiveness_report(path: Path, text: str) -> list[Finding]:
646778 return findings
647779
648780
649- def validate_task_outcome (path : Path , text : str ) -> list [Finding ]:
781+ def validate_task_outcome (root : Path , path : Path , text : str ) -> list [Finding ]:
650782 report_include_value = yaml_field_value (text , "include_in_effectiveness_report" )
651783 comparable_count_value = yaml_field_value (
652784 text , "include_in_comparable_product_task_count"
@@ -679,6 +811,22 @@ def validate_task_outcome(path: Path, text: str) -> list[Finding]:
679811 )
680812 )
681813
814+ verification_command = yaml_field_value (text , "verification_command" )
815+ for command in missing_make_commands (root , verification_command ):
816+ findings .append (
817+ Finding (
818+ path ,
819+ f"task outcome verification references missing Makefile target: { command } " ,
820+ )
821+ )
822+ for command in missing_just_commands (root , verification_command ):
823+ findings .append (
824+ Finding (
825+ path ,
826+ f"task outcome verification references missing justfile recipe: { command } " ,
827+ )
828+ )
829+
682830 truthy_include_fields = [
683831 field
684832 for field in TASK_OUTCOME_INCLUDE_FIELDS
@@ -790,7 +938,9 @@ def check_effectiveness_plan(root: Path, require_report: bool) -> int:
790938 findings .extend (validate_effectiveness_report (path , text ))
791939
792940 for path in iter_task_outcomes (root ):
793- findings .extend (validate_task_outcome (path , path .read_text (encoding = "utf-8" )))
941+ findings .extend (
942+ validate_task_outcome (root , path , path .read_text (encoding = "utf-8" ))
943+ )
794944
795945 for finding in findings :
796946 print (f"{ finding .path .relative_to (root )} : { finding .message } " )
0 commit comments