diff --git a/AGENTS.md b/AGENTS.md index 91a533ef7..6849f4869 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 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. + **15. Explicit codes with \(d\leq 5\) get a small-distance parent.** An entry for one explicit code whose distance is at most five also lists a small-distance family as a parent. Use `small_distance_qubit_stabilizer` for @@ -265,7 +282,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/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 eb48ba665..5b34a0cb3 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 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. @@ -22,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 @@ -227,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 = ( @@ -239,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", ) @@ -388,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({ @@ -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] = [] @@ -539,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() @@ -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,57 @@ 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 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 + 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]]] = [] + 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 +696,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]]] = []