Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code_id>
```

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.

## Relation semantics

- Relations are declared on the CHILD, in its `relations.parents` list; the
Expand Down Expand Up @@ -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 <code_id>`, 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
Expand Down
2 changes: 1 addition & 1 deletion codelists/list_all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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', ...]
Comment thread
valbert4 marked this conversation as resolved.
# manual_code_list: ['code_id_1', 'code_id_2', ...]
#
# exclude: ['code_id_1', 'code_id_2', ...]
#
Expand Down
93 changes: 83 additions & 10 deletions scripts/codelists/count_list_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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 = (
Expand All @@ -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",
)


Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -446,16 +452,16 @@ 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]],
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)."""
) -> 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] = []
Expand Down Expand Up @@ -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()
Expand All @@ -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


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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()

Expand All @@ -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]]] = []
Expand Down
Loading