1111# python3 scripts/codelists/count_list_codes.py # sort by count descending
1212# python3 scripts/codelists/count_list_codes.py --sort count --reverse
1313# python3 scripts/codelists/count_list_codes.py --sort properties
14+ # python3 scripts/codelists/count_list_codes.py --code 3d_xyz_color
15+ #
16+ # With --code X, print instead the lists that contain X, followed by the lists
17+ # that contain a child or cousin of X but not X itself. Check that
18+ # second group after adding or reparenting a code: it is where a code is most
19+ # often missed (AGENTS.md rule 14).
1420#
1521# Codelist select semantics – each select entry is a condition group.
1622# Within a group all conditions are ANDed; between groups they are ORed.
2228# cousin_of: X – candidates = cousin-neighbours of X (both dirs)
2329# property_set: a.b – candidates = codes whose YAML has the dotted key set
2430# domain: D – candidates = codes in domain D
25- # manual_code_ids : [A,B] – explicit code list
31+ # manual_code_list : [A,B] – explicit code list
2632# not_descendant_of: X – remove descendants of X from candidates
2733# not_all_descendant_of: [A,B] – remove codes that are descendants of ALL of A,B
2834
@@ -227,7 +233,7 @@ def _descendants(roots: list[str], p2c: dict[str, list[str]]) -> set[str]:
227233 "any_cousin_of" , "all_cousin_of" ,
228234 "not_any_cousin_of" , "not_all_cousin_of" ,
229235 "any_domain" , "not_any_domain" ,
230- "manual_code_ids " , "exclude" ,
236+ "manual_code_list " , "exclude" ,
231237})
232238
233239_SELECTORS = (
@@ -239,7 +245,7 @@ def _descendants(roots: list[str], p2c: dict[str, list[str]]) -> set[str]:
239245 "cousin_of" ,
240246 "property_set" ,
241247 "not_any_domain" , "any_domain" , "not_domain" , "domain" ,
242- "manual_code_ids " , "exclude" ,
248+ "manual_code_list " , "exclude" ,
243249)
244250
245251
@@ -388,7 +394,7 @@ def _add(group: dict[str, list[str]], sel: str, raw_val: str) -> None:
388394 "cousin_of" , "any_cousin_of" , "all_cousin_of" ,
389395 "property_set" ,
390396 "domain" , "any_domain" ,
391- "manual_code_ids " ,
397+ "manual_code_list " ,
392398})
393399
394400_NEGATIVE_KEYS = frozenset ({
@@ -446,16 +452,16 @@ def _inter_cousin(cids: list[str], cout: dict, cin: dict, known: set,
446452 return result if result is not None else set ()
447453
448454
449- def count_codes (
455+ def list_members (
450456 groups : list [dict [str , list [str ]]],
451457 p2c : dict [str , list [str ]],
452458 cout : dict [str , set [str ]],
453459 cin : dict [str , set [str ]],
454460 code_props : dict [str , frozenset [str ]],
455461 code_domain : dict [str , str ],
456462 known : set [str ],
457- ) -> tuple [int , list [str ]]:
458- """Return (count , warnings)."""
463+ ) -> tuple [set [ str ] , list [str ]]:
464+ """Return (set of code_ids in the list , warnings)."""
459465 accumulated : set [str ] = set ()
460466 global_exclude : set [str ] = set ()
461467 warnings : list [str ] = []
@@ -539,8 +545,8 @@ def _ix(s: set[str]) -> None:
539545 dom_set = set (group ["any_domain" ])
540546 _ix ({c for c , d in code_domain .items () if d in dom_set })
541547
542- if "manual_code_ids " in group :
543- _ix ({cid for cid in group ["manual_code_ids " ] if cid in known })
548+ if "manual_code_list " in group :
549+ _ix ({cid for cid in group ["manual_code_list " ] if cid in known })
544550
545551 if candidates is None :
546552 candidates = set ()
@@ -566,7 +572,22 @@ def _ix(s: set[str]) -> None:
566572
567573 accumulated |= candidates
568574
569- return len (accumulated - global_exclude ), warnings
575+ return accumulated - global_exclude , warnings
576+
577+
578+ def count_codes (
579+ groups : list [dict [str , list [str ]]],
580+ p2c : dict [str , list [str ]],
581+ cout : dict [str , set [str ]],
582+ cin : dict [str , set [str ]],
583+ code_props : dict [str , frozenset [str ]],
584+ code_domain : dict [str , str ],
585+ known : set [str ],
586+ ) -> tuple [int , list [str ]]:
587+ """Return (count, warnings)."""
588+ members , warnings = list_members (
589+ groups , p2c , cout , cin , code_props , code_domain , known )
590+ return len (members ), warnings
570591
571592
572593# ---------------------------------------------------------------------------
@@ -599,9 +620,57 @@ def _parse_args() -> argparse.Namespace:
599620 "codelists/features relative to the repo root."
600621 ),
601622 )
623+ parser .add_argument (
624+ "--code" ,
625+ default = None ,
626+ metavar = "CODE_ID" ,
627+ help = (
628+ "Print the lists that contain CODE_ID, then the lists that contain "
629+ "a child or cousin of CODE_ID but not CODE_ID itself."
630+ ),
631+ )
602632 return parser .parse_args ()
603633
604634
635+ def report_code (
636+ code_id : str ,
637+ list_files : list [Path ],
638+ repo_root : Path ,
639+ p2c : dict [str , list [str ]],
640+ cout : dict [str , set [str ]],
641+ cin : dict [str , set [str ]],
642+ code_props : dict [str , frozenset [str ]],
643+ code_domain : dict [str , str ],
644+ known : set [str ],
645+ ) -> int :
646+ if code_id not in known :
647+ print (f"Unknown code_id '{ code_id } '" , file = sys .stderr )
648+ return 1
649+ relatives = set (p2c .get (code_id , [])) | cout .get (code_id , set ()) \
650+ | cin .get (code_id , set ())
651+ inside : list [tuple [str , str ]] = []
652+ missing : list [tuple [str , str , list [str ]]] = []
653+ for lf in list_files :
654+ title , groups , _cols = _parse_codelist (str (lf ))
655+ members , _w = list_members (
656+ groups , p2c , cout , cin , code_props , code_domain , known )
657+ rel = str (lf .relative_to (repo_root ))
658+ if code_id in members :
659+ inside .append ((rel , title ))
660+ else :
661+ hits = sorted (relatives & members )
662+ if hits :
663+ missing .append ((rel , title , hits ))
664+ print (f"Lists containing { code_id } ({ len (inside )} ):" )
665+ for rel , title in inside :
666+ print (f" { title } [{ rel } ]" )
667+ print (f"\n Lists containing a relative of { code_id } but not { code_id } "
668+ f"({ len (missing )} ); check whether it belongs:" )
669+ for rel , title , hits in missing :
670+ print (f" { title } [{ rel } ] via { ', ' .join (hits )} " )
671+ return 0
672+
673+
605674def main () -> int :
606675 args = _parse_args ()
607676
@@ -627,6 +696,10 @@ def main() -> int:
627696 print (f"No YAML files found under { lists_dirs } " , file = sys .stderr )
628697 return 1
629698
699+ if args .code :
700+ return report_code (args .code , list_files , repo_root , p2c , cout , cin ,
701+ code_props , code_domain , known )
702+
630703 print (f"Total number of lists: { len (list_files )} \n " )
631704
632705 rows : list [tuple [str , str , int , list [str ]]] = []
0 commit comments