From dfa597b3cb5ff304d689b00657bf529d80ca10f0 Mon Sep 17 00:00:00 2001 From: Tamara Date: Tue, 13 Oct 2026 09:47:29 -0400 Subject: [PATCH 1/3] AGENTS.md rule 14 and count_list_codes --code: check code lists for new or reparented codes - count_list_codes.py: list_members() returns the member set (count_codes wraps it); --code prints the lists containing the code, then the lists that contain a parent, child, or cousin of it but not the code itself - AGENTS.md: rule 14 asks for that check after adding a code or changing relations, and documents the option under scripts/codelists/ Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 21 ++++++- scripts/codelists/count_list_codes.py | 82 +++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6f585cdae..1c7bb26fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,6 +145,23 @@ lint scripts `spellcheck.py`, `remove_trailing_block_apostrophes.py`, and `scripts/params/check_tableaux.py`. This is the routine-check list in `scripts/script_list_for_checking.txt`. +**14. Check the code lists after adding a code or changing its relations.** The +lists in `codelists/` select codes mostly by descendancy and cousinhood (e.g. +`descendant_of: color`). A code placed outside the expected branch therefore +drops out of a list where it belongs, and nothing reports it. For each new or +reparented code, run + +``` +python3 scripts/codelists/count_list_codes.py --code +``` + +It prints the lists that contain the code, then the lists that contain one of +its parents, children, or cousins but not the code itself. Go through the second +group. Where the code belongs in a list, fix its relations or add it to that +list's `select`, and say so in the PR. E.g. `3d_xyz_color` has no color-code +ancestor, so `list_color` (descendants of `color` and `twist_defect_color`) +misses it. + ## Relation semantics - Relations are declared on the CHILD, in its `relations.parents` list; the @@ -253,7 +270,9 @@ describing its exact behavior and usage. - `scripts/bib/` — bibliography/citation tooling: extract arXiv IDs, DOIs, manual cites and presets; append reference counts. - `scripts/queries/` — external lookups: `query_qecdb`, `semantic_scholar_recommend`. -- `scripts/codelists/` — code-list tooling: `count_list_codes`. +- `scripts/codelists/` — code-list tooling: `count_list_codes` (list sizes; with + `--code `, the lists that contain a code and the lists that miss it + while containing a relative, per rule 14). - `scripts/params/` — code-parameter checks (Python >= 3.10 and `PyYAML` only). `check_tableaux.py` (routine) reads every Pauli tableau displayed in an entry (`array`/`smallmatrix` blocks with I/X/Y/Z cells, and "cyclic permutations of diff --git a/scripts/codelists/count_list_codes.py b/scripts/codelists/count_list_codes.py index eb48ba665..5bad167a1 100644 --- a/scripts/codelists/count_list_codes.py +++ b/scripts/codelists/count_list_codes.py @@ -11,6 +11,12 @@ # python3 scripts/codelists/count_list_codes.py # sort by count descending # python3 scripts/codelists/count_list_codes.py --sort count --reverse # python3 scripts/codelists/count_list_codes.py --sort properties +# python3 scripts/codelists/count_list_codes.py --code 3d_xyz_color +# +# With --code X, print instead the lists that contain X, followed by the lists +# that contain a parent, child, or cousin of X but not X itself. Check that +# second group after adding or reparenting a code: it is where a code is most +# often missed (AGENTS.md rule 14). # # Codelist select semantics – each select entry is a condition group. # Within a group all conditions are ANDed; between groups they are ORed. @@ -446,7 +452,7 @@ def _inter_cousin(cids: list[str], cout: dict, cin: dict, known: set, return result if result is not None else set() -def count_codes( +def list_members( groups: list[dict[str, list[str]]], p2c: dict[str, list[str]], cout: dict[str, set[str]], @@ -454,8 +460,8 @@ def count_codes( code_props: dict[str, frozenset[str]], code_domain: dict[str, str], known: set[str], -) -> tuple[int, list[str]]: - """Return (count, warnings).""" +) -> tuple[set[str], list[str]]: + """Return (set of code_ids in the list, warnings).""" accumulated: set[str] = set() global_exclude: set[str] = set() warnings: list[str] = [] @@ -566,7 +572,22 @@ def _ix(s: set[str]) -> None: accumulated |= candidates - return len(accumulated - global_exclude), warnings + return accumulated - global_exclude, warnings + + +def count_codes( + groups: list[dict[str, list[str]]], + p2c: dict[str, list[str]], + cout: dict[str, set[str]], + cin: dict[str, set[str]], + code_props: dict[str, frozenset[str]], + code_domain: dict[str, str], + known: set[str], +) -> tuple[int, list[str]]: + """Return (count, warnings).""" + members, warnings = list_members( + groups, p2c, cout, cin, code_props, code_domain, known) + return len(members), warnings # --------------------------------------------------------------------------- @@ -599,9 +620,58 @@ def _parse_args() -> argparse.Namespace: "codelists/features relative to the repo root." ), ) + parser.add_argument( + "--code", + default=None, + metavar="CODE_ID", + help=( + "Print the lists that contain CODE_ID, then the lists that contain " + "a parent, child, or cousin of CODE_ID but not CODE_ID itself." + ), + ) return parser.parse_args() +def report_code( + code_id: str, + list_files: list[Path], + repo_root: Path, + p2c: dict[str, list[str]], + cout: dict[str, set[str]], + cin: dict[str, set[str]], + code_props: dict[str, frozenset[str]], + code_domain: dict[str, str], + known: set[str], +) -> int: + if code_id not in known: + print(f"Unknown code_id '{code_id}'", file=sys.stderr) + return 1 + parents = {p for p, kids in p2c.items() if code_id in kids} + relatives = parents | set(p2c.get(code_id, [])) | cout.get(code_id, set()) \ + | cin.get(code_id, set()) + inside: list[tuple[str, str]] = [] + missing: list[tuple[str, str, list[str]]] = [] + for lf in list_files: + title, groups, _cols = _parse_codelist(str(lf)) + members, _w = list_members( + groups, p2c, cout, cin, code_props, code_domain, known) + rel = str(lf.relative_to(repo_root)) + if code_id in members: + inside.append((rel, title)) + else: + hits = sorted(relatives & members) + if hits: + missing.append((rel, title, hits)) + print(f"Lists containing {code_id} ({len(inside)}):") + for rel, title in inside: + print(f" {title} [{rel}]") + print(f"\nLists containing a relative of {code_id} but not {code_id} " + f"({len(missing)}); check whether it belongs:") + for rel, title, hits in missing: + print(f" {title} [{rel}] via {', '.join(hits)}") + return 0 + + def main() -> int: args = _parse_args() @@ -627,6 +697,10 @@ def main() -> int: print(f"No YAML files found under {lists_dirs}", file=sys.stderr) return 1 + if args.code: + return report_code(args.code, list_files, repo_root, p2c, cout, cin, + code_props, code_domain, known) + print(f"Total number of lists: {len(list_files)}\n") rows: list[tuple[str, str, int, list[str]]] = [] From 3170ed20b4bbdbbde083663f790700e5bdd0b6f2 Mon Sep 17 00:00:00 2001 From: Tamara Date: Tue, 13 Oct 2026 09:50:15 -0400 Subject: [PATCH 2/3] count_list_codes: use the site's manual_code_list selector; --code checks children and cousins only The site build accepts only manual_code_list (its predicate schema rejects manual_code_ids), so the script and the list_all.yml comment now use it. Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 2 +- codelists/list_all.yml | 2 +- scripts/codelists/count_list_codes.py | 19 +++++++++---------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1c7bb26fa..8c1b83edc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -156,7 +156,7 @@ python3 scripts/codelists/count_list_codes.py --code ``` It prints the lists that contain the code, then the lists that contain one of -its parents, children, or cousins but not the code itself. Go through the second +its children or cousins but not the code itself. Go through the second group. Where the code belongs in a list, fix its relations or add it to that list's `select`, and say so in the PR. E.g. `3d_xyz_color` has no color-code ancestor, so `list_color` (descendants of `color` and `twist_defect_color`) diff --git a/codelists/list_all.yml b/codelists/list_all.yml index 55666ebf0..4d7eef844 100644 --- a/codelists/list_all.yml +++ b/codelists/list_all.yml @@ -54,7 +54,7 @@ codes: # Possible predicate types to be included in each "select" code set include: # - # manual_code_ids: ['code_id_1', 'code_id_2', ...] + # manual_code_list: ['code_id_1', 'code_id_2', ...] # # exclude: ['code_id_1', 'code_id_2', ...] # diff --git a/scripts/codelists/count_list_codes.py b/scripts/codelists/count_list_codes.py index 5bad167a1..5b34a0cb3 100644 --- a/scripts/codelists/count_list_codes.py +++ b/scripts/codelists/count_list_codes.py @@ -14,7 +14,7 @@ # python3 scripts/codelists/count_list_codes.py --code 3d_xyz_color # # With --code X, print instead the lists that contain X, followed by the lists -# that contain a parent, child, or cousin of X but not X itself. Check that +# that contain a child or cousin of X but not X itself. Check that # second group after adding or reparenting a code: it is where a code is most # often missed (AGENTS.md rule 14). # @@ -28,7 +28,7 @@ # cousin_of: X – candidates = cousin-neighbours of X (both dirs) # property_set: a.b – candidates = codes whose YAML has the dotted key set # domain: D – candidates = codes in domain D -# manual_code_ids: [A,B] – explicit code list +# manual_code_list: [A,B] – explicit code list # not_descendant_of: X – remove descendants of X from candidates # not_all_descendant_of: [A,B] – remove codes that are descendants of ALL of A,B @@ -233,7 +233,7 @@ def _descendants(roots: list[str], p2c: dict[str, list[str]]) -> set[str]: "any_cousin_of", "all_cousin_of", "not_any_cousin_of", "not_all_cousin_of", "any_domain", "not_any_domain", - "manual_code_ids", "exclude", + "manual_code_list", "exclude", }) _SELECTORS = ( @@ -245,7 +245,7 @@ def _descendants(roots: list[str], p2c: dict[str, list[str]]) -> set[str]: "cousin_of", "property_set", "not_any_domain", "any_domain", "not_domain", "domain", - "manual_code_ids", "exclude", + "manual_code_list", "exclude", ) @@ -394,7 +394,7 @@ def _add(group: dict[str, list[str]], sel: str, raw_val: str) -> None: "cousin_of", "any_cousin_of", "all_cousin_of", "property_set", "domain", "any_domain", - "manual_code_ids", + "manual_code_list", }) _NEGATIVE_KEYS = frozenset({ @@ -545,8 +545,8 @@ def _ix(s: set[str]) -> None: dom_set = set(group["any_domain"]) _ix({c for c, d in code_domain.items() if d in dom_set}) - if "manual_code_ids" in group: - _ix({cid for cid in group["manual_code_ids"] if cid in known}) + if "manual_code_list" in group: + _ix({cid for cid in group["manual_code_list"] if cid in known}) if candidates is None: candidates = set() @@ -626,7 +626,7 @@ def _parse_args() -> argparse.Namespace: metavar="CODE_ID", help=( "Print the lists that contain CODE_ID, then the lists that contain " - "a parent, child, or cousin of CODE_ID but not CODE_ID itself." + "a child or cousin of CODE_ID but not CODE_ID itself." ), ) return parser.parse_args() @@ -646,8 +646,7 @@ def report_code( if code_id not in known: print(f"Unknown code_id '{code_id}'", file=sys.stderr) return 1 - parents = {p for p, kids in p2c.items() if code_id in kids} - relatives = parents | set(p2c.get(code_id, [])) | cout.get(code_id, set()) \ + relatives = set(p2c.get(code_id, [])) | cout.get(code_id, set()) \ | cin.get(code_id, set()) inside: list[tuple[str, str]] = [] missing: list[tuple[str, str, list[str]]] = [] From 82186e8ea4613c6187cfea2520a5e8946897e7a2 Mon Sep 17 00:00:00 2001 From: Tamara Date: Tue, 13 Oct 2026 21:29:32 -0400 Subject: [PATCH 3/3] Retrigger CI after merging main Co-Authored-By: Claude Opus 5.5 (1M context)