diff --git a/README.md b/README.md index b0551db..d4487c3 100644 --- a/README.md +++ b/README.md @@ -424,7 +424,9 @@ Resolution order for `microservice`: ### Re-index required when ontology changes -Current ontology version is **14**. Any index built before this version must be rebuilt via `cocoindex update ... --full-reprocess -f` or a full `java-codebase-rag reprocess` (no selective flags) so vectors and graph stay aligned. Until re-indexed, the server defensively JSON-decodes string-form list columns so nothing explodes, but filters like `array_contains` will not work. +Current ontology version is **15**. Any index built before this version must be rebuilt via `cocoindex update ... --full-reprocess -f` or a full `java-codebase-rag reprocess` (no selective flags) so vectors and graph stay aligned. Until re-indexed, the server defensively JSON-decodes string-form list columns so nothing explodes, but filters like `array_contains` will not work. + +Ontology **15** (CALLS-NOISE PR-1) adds `CALLS.callee_declaring_role`, `GraphMeta.pass3_unresolved_phantom_receiver` / `pass3_unresolved_chained`, and **supertype-walk dedup** at build time: duplicate interface + concrete candidates at the same call site collapse to one `CALLS` row (row counts per method may drop after re-index, not only a new column). PR-2 adds `edge_filter` on `neighbors`; PR-3 moves true receiver-failure rows off `CALLS`. Ontology **14** introduces `EDGE_SCHEMA` in `java_ontology.py` as the canonical edge navigation schema (see `docs/EDGE-NAVIGATION.md`). **`HTTP_CALLS` is `Client → Route`** (SCHEMA-V2 PR-B). **`ASYNC_CALLS` is `Producer → Route`** with `DECLARES_PRODUCER` (SCHEMA-V2 PR-C). Run one full reprocess after upgrading through the SCHEMA-V2 sequence (or when you need the v14 ontology gate). diff --git a/ast_java.py b/ast_java.py index 8b83a89..f82d688 100644 --- a/ast_java.py +++ b/ast_java.py @@ -81,8 +81,9 @@ # Phase 9: `@CodebaseAsyncRoute` replaces same-method built-in `@KafkaListener` routes in graph composition. # Phase 10: `@CodebaseHttpClient` rename + `CodebaseHttpMethod` enum; inbound HTTP layer-C replaces built-in rows. # Phase 11: `EDGE_SCHEMA` in `java_ontology.py` (canonical edge navigation schema; v14 re-index). +# Phase 12: CALLS `callee_declaring_role`, supertype-walk dedup, pass3 unresolved counters (v15 re-index). # Bumps whenever extraction / enrichment semantics change. -ONTOLOGY_VERSION = 14 +ONTOLOGY_VERSION = 15 ROLE_ANNOTATIONS: dict[str, str] = { # Spring Web diff --git a/build_ast_graph.py b/build_ast_graph.py index aa01e04..97fdbad 100644 --- a/build_ast_graph.py +++ b/build_ast_graph.py @@ -180,6 +180,7 @@ class CallsRow: strategy: str = "phantom" source: str = "static" resolved: bool = True + callee_declaring_role: str = "OTHER" @dataclass @@ -380,7 +381,11 @@ class GraphTables: parse_errors: int = 0 skipped_files: int = 0 pass3_skipped_cross_service: int = 0 + pass3_unresolved_phantom_receiver: int = 0 + pass3_unresolved_chained: int = 0 cross_service_resolution: str = "auto" + # Populated in _write_nodes (same overrides + meta_chain as Symbol.role). + type_role_by_node_id: dict[str, str] = field(default_factory=dict) # ---------- file walk (see `path_filtering.iter_java_source_files`) ---------- @@ -1129,6 +1134,81 @@ def _phantom_method_id( return pid +def _method_signature_matches_call(member: MemberEntry, call: CallSite) -> bool: + if call.arg_count < 0: + return True + return len(member.decl.parameters) == call.arg_count + + +def _is_strict_supertype_of(tables: GraphTables, super_fqn: str, subtype_fqn: str) -> bool: + if super_fqn == subtype_fqn: + return False + entry = tables.types.get(subtype_fqn) + if entry is None: + return False + visited: set[str] = set() + queue = list(_direct_supertype_fqns(entry, tables)) + while queue: + tfqn = queue.pop(0) + if tfqn == super_fqn: + return True + if tfqn in visited or tfqn not in tables.types: + continue + visited.add(tfqn) + queue.extend(_direct_supertype_fqns(tables.types[tfqn], tables)) + return False + + +def _callee_declaring_role_at_write( + tables: GraphTables, + dst_id: str, + *, + member_by_id: dict[str, MemberEntry], +) -> str: + """Match parent declaring-type Symbol.role (brownfield + meta_chain included).""" + if dst_id in tables.phantoms: + return "OTHER" + member = member_by_id.get(dst_id) + if member is None: + return "OTHER" + return tables.type_role_by_node_id.get(member.parent_id, "OTHER") + + +def _collapse_supertype_duplicates( + candidates: list[MemberEntry], + recv_type_fqn: str, + call: CallSite, + tables: GraphTables, +) -> list[MemberEntry]: + """§3.3.1 supertype-walk dedup — collapse interface + concrete duplicate sites.""" + if len(candidates) <= 1: + return candidates + concrete_on_receiver = [ + c for c in candidates + if c.parent_fqn == recv_type_fqn and _method_signature_matches_call(c, call) + ] + if len(concrete_on_receiver) != 1: + return candidates + concrete = concrete_on_receiver[0] + supertypes = [ + c for c in candidates + if c is not concrete + and _is_strict_supertype_of(tables, c.parent_fqn, recv_type_fqn) + and c.decl.signature == concrete.decl.signature + ] + if not supertypes: + return candidates + allowed_ids = {concrete.node_id, *(c.node_id for c in supertypes)} + if any(c.node_id not in allowed_ids for c in candidates): + return candidates + log.debug( + "pass3 supertype dedup %s -> %s", + [c.node_id for c in candidates], + concrete.node_id, + ) + return [concrete] + + def _emit_call_edge( tables: GraphTables, stats: CallResolutionStats, @@ -1269,6 +1349,9 @@ def _resolve_and_emit_call( ) return + if len(candidates) > 1 and edge_strat != "overload_ambiguous": + candidates = _collapse_supertype_duplicates(candidates, recv_type, call, tables) + if len(candidates) == 1: candidate = candidates[0] ref_arity: int | None = None @@ -1334,6 +1417,8 @@ def pass3_calls(tables: GraphTables, asts: dict[str, JavaFileAst], *, verbose: b pct_callee_unres = 100.0 * stats.callee_unresolved / max(1, stats.total) pct_phantom_recv = 100.0 * stats.phantom_other / max(1, stats.total) tables.pass3_skipped_cross_service = int(stats.skipped_cross_service) + tables.pass3_unresolved_phantom_receiver = int(stats.phantom_other) + tables.pass3_unresolved_chained = int(stats.phantom_chained) msg = ( f"Call resolution: {stats.total} sites, {stats.phantom_chained} chained phantoms " f"({pct_chained:.1f}%), {stats.callee_unresolved} unresolved callee " @@ -2262,6 +2347,8 @@ def _micro_factor(member: MemberEntry | None) -> float: "async_calls_match_breakdown STRING, " "cross_service_calls_total INT64, " "pass3_skipped_cross_service INT64, " + "pass3_unresolved_phantom_receiver INT64, " + "pass3_unresolved_chained INT64, " "pass4_exposes_suppressed_feign INT64, " "cross_service_resolution STRING" ")" @@ -2316,7 +2403,8 @@ def _micro_factor(member: MemberEntry | None) -> float: _SCHEMA_CALLS = ( "CREATE REL TABLE CALLS(FROM Symbol TO Symbol, " "call_site_line INT64, call_site_byte INT64, arg_count INT64, " - "confidence DOUBLE, strategy STRING, source STRING, resolved BOOLEAN)" + "confidence DOUBLE, strategy STRING, source STRING, resolved BOOLEAN, " + "callee_declaring_role STRING)" ) _SCHEMA_EXPOSES = ( "CREATE REL TABLE EXPOSES(FROM Symbol TO Route, " @@ -2445,6 +2533,7 @@ def _write_nodes( overrides=overrides, meta_chain=mch, ) + tables.type_role_by_node_id[entry.node_id] = role conn.execute(_CREATE_SYMBOL, _node_row( id=entry.node_id, kind=d.kind, name=d.name, fqn=d.fqn, package=entry.package, @@ -2503,7 +2592,8 @@ def _write_nodes( "MATCH (a:Symbol {id: $src}), (b:Symbol {id: $dst}) " "CREATE (a)-[:CALLS {" "call_site_line: $line, call_site_byte: $byte, arg_count: $argc, " - "confidence: $conf, strategy: $strat, source: $src_kind, resolved: $resolved" + "confidence: $conf, strategy: $strat, source: $src_kind, resolved: $resolved, " + "callee_declaring_role: $callee_declaring_role" "}]->(b)" ) @@ -2637,6 +2727,7 @@ def _write_edges(conn: kuzu.Connection, tables: GraphTables) -> None: seen_calls.add(key) unique_calls.append(row) + member_by_id = {m.node_id: m for m in tables.members} for row in unique_calls: conn.execute(_CREATE_CALL, { "src": row.src_id, "dst": row.dst_id, @@ -2647,6 +2738,9 @@ def _write_edges(conn: kuzu.Connection, tables: GraphTables) -> None: "strat": row.strategy, "src_kind": row.source, "resolved": row.resolved, + "callee_declaring_role": _callee_declaring_role_at_write( + tables, row.dst_id, member_by_id=member_by_id, + ), }) @@ -2788,6 +2882,8 @@ def _write_meta(conn: kuzu.Connection, tables: GraphTables, source_root: Path) - "async_calls_match_breakdown: $async_calls_match_breakdown, " "cross_service_calls_total: $cross_service_calls_total, " "pass3_skipped_cross_service: $pass3_skipped_cross_service, " + "pass3_unresolved_phantom_receiver: $pass3_unresolved_phantom_receiver, " + "pass3_unresolved_chained: $pass3_unresolved_chained, " "pass4_exposes_suppressed_feign: $pass4_exposes_suppressed_feign, " "cross_service_resolution: $cross_service_resolution})", { @@ -2821,6 +2917,8 @@ def _write_meta(conn: kuzu.Connection, tables: GraphTables, source_root: Path) - "async_calls_match_breakdown": json.dumps(async_match), "cross_service_calls_total": int(call_stats.cross_service_calls_total), "pass3_skipped_cross_service": int(tables.pass3_skipped_cross_service), + "pass3_unresolved_phantom_receiver": int(tables.pass3_unresolved_phantom_receiver), + "pass3_unresolved_chained": int(tables.pass3_unresolved_chained), "pass4_exposes_suppressed_feign": int(st.exposes_suppressed_feign), "cross_service_resolution": str(tables.cross_service_resolution), }, diff --git a/docs/AGENT-GUIDE.md b/docs/AGENT-GUIDE.md index 5c5296b..8cdb32b 100644 --- a/docs/AGENT-GUIDE.md +++ b/docs/AGENT-GUIDE.md @@ -12,10 +12,12 @@ > `neighbors` arguments, pass stringified JSON, or use vector search for > questions the graph answers exactly. This guide keeps them on the rails. > -> Calibrated against ontology version **14** (see `ast_java.ONTOLOGY_VERSION` / +> Calibrated against ontology version **15** (see `ast_java.ONTOLOGY_VERSION` / > `java_ontology.EDGE_SCHEMA` + valid sets): canonical edge navigation schema in -> `docs/EDGE-NAVIGATION.md`. v14 re-index required; `HTTP_CALLS` is `Client → Route`; -> `Producer` + `DECLARES_PRODUCER` and `ASYNC_CALLS` (`Producer → Route`) ship in v14. +> `docs/EDGE-NAVIGATION.md`. v15 re-index required — `CALLS.callee_declaring_role`, +> supertype-walk dedup (fewer duplicate-site rows), and `GraphMeta` pass3 unresolved +> counters; PR-2 adds `edge_filter` on `neighbors`. v14: `HTTP_CALLS` is `Client → Route`; +> `Producer` + `DECLARES_PRODUCER` and `ASYNC_CALLS` (`Producer → Route`). > Still includes stored `OVERRIDES` Symbol→Symbol edges and v12 HTTP brownfield > (`@CodebaseHttpClient`, shared `CodebaseHttpMethod` enum, inbound layer-C HTTP routes > replace same-method built-in rows). **Design rationale:** navigation surface and tools — @@ -264,7 +266,7 @@ Virtual keys (`OVERRIDDEN_BY`, …) are **not** valid `neighbors` arguments — - **Mixed flat + composed `edge_types`:** flat edges are appended before composed edges, then `limit`/`offset` apply. A small `limit` with e.g. `["DECLARES", "DECLARES.DECLARES_CLIENT"]` may return only member Symbols and no Clients — use the dot-key alone when enumerating terminals. - **Confidence:** Cross-service edges (`HTTP_CALLS`, `ASYNC_CALLS`) carry confidence, strategy, and match metadata on `edge.attrs` (`attrs.confidence`, `attrs.strategy`, `attrs.match`). Low confidence means the resolver had to guess at the route binding — treat it as a **resolver gap signal**, not a hallucination. Report low-confidence edges with their confidence value, not as facts. Intra-service edges (`CALLS`, `INJECTS`, `IMPLEMENTS`, `EXTENDS`, `DECLARES`, `DECLARES_CLIENT`, `EXPOSES`, `OVERRIDES`) faithfully represent the static graph; the resolved set is still a **lower bound** under reflection / dynamic dispatch (see *What this MCP is NOT*). -### Ontology glossary (version 14) +### Ontology glossary (version 15) Source of truth: `java_ontology.py` (`EDGE_SCHEMA`, valid sets). Strings are case-sensitive. Edge navigation: [`docs/EDGE-NAVIGATION.md`](./EDGE-NAVIGATION.md) — for `HTTP_CALLS`, traverse via `DECLARES_CLIENT` from a method Symbol or `neighbors` outbound from a Client id; for `ASYNC_CALLS`, traverse via `DECLARES_PRODUCER` or outbound from a Producer id. diff --git a/docs/EDGE-NAVIGATION.md b/docs/EDGE-NAVIGATION.md index cdacc21..a07ed08 100644 --- a/docs/EDGE-NAVIGATION.md +++ b/docs/EDGE-NAVIGATION.md @@ -137,6 +137,7 @@ - `strategy` (`STRING`) — call-graph resolution strategy literal - `source` (`STRING`) — call-graph source tag - `resolved` (`BOOLEAN`) — True iff callee Symbol was resolved in-graph +- `callee_declaring_role` (`STRING`) — role of the Symbol that declares the callee method **Typical traversals**: diff --git a/java_ontology.py b/java_ontology.py index ad3dcc7..9d2486f 100644 --- a/java_ontology.py +++ b/java_ontology.py @@ -261,6 +261,11 @@ class EdgeSpec: EdgeAttr("strategy", "STRING", "call-graph resolution strategy literal"), EdgeAttr("source", "STRING", "call-graph source tag"), EdgeAttr("resolved", "BOOLEAN", "True iff callee Symbol was resolved in-graph"), + EdgeAttr( + "callee_declaring_role", + "STRING", + "role of the Symbol that declares the callee method", + ), ), purpose="intra-codebase method call from caller method to callee method", member_only=True, diff --git a/tests/fixtures/call_graph_smoke/src/main/java/smoke/SupertypeDedupPatterns.java b/tests/fixtures/call_graph_smoke/src/main/java/smoke/SupertypeDedupPatterns.java new file mode 100644 index 0000000..f782b54 --- /dev/null +++ b/tests/fixtures/call_graph_smoke/src/main/java/smoke/SupertypeDedupPatterns.java @@ -0,0 +1,29 @@ +package smoke; + +/** Minimal interface + concrete same-site stub for pass3 supertype-walk dedup (PR-1). */ +@interface Repository { +} + +@Repository +interface JpaStyleRepo { + void save(Object entity); +} + +@Repository +class JpaStyleRepoImpl implements JpaStyleRepo { + @Override + public void save(Object entity) { + } +} + +public class SupertypeDedupPatterns { + private final JpaStyleRepoImpl repo; + + SupertypeDedupPatterns(JpaStyleRepoImpl repo) { + this.repo = repo; + } + + void persist(Object entity) { + repo.save(entity); + } +} diff --git a/tests/test_ast_graph_build.py b/tests/test_ast_graph_build.py index 50f1187..d571a56 100644 --- a/tests/test_ast_graph_build.py +++ b/tests/test_ast_graph_build.py @@ -16,7 +16,9 @@ import kuzu import pytest +from _builders import build_kuzu_to from ast_java import ONTOLOGY_VERSION +from graph_enrich import _load_brownfield_overrides, collect_annotation_meta_chain def _connect(db_path: Path) -> kuzu.Connection: @@ -58,6 +60,96 @@ def test_schema_has_all_expected_tables(kuzu_db_path: Path) -> None: assert not missing, f"missing schema tables: {missing}; saw {tables}" +def test_graph_meta_unresolved_counters_present(kuzu_db_path: Path) -> None: + conn = _connect(kuzu_db_path) + r = conn.execute( + "MATCH (m:GraphMeta) RETURN m.pass3_unresolved_phantom_receiver, " + "m.pass3_unresolved_chained" + ) + assert r.has_next(), "expected GraphMeta row" + row = r.get_next() + assert row[0] is not None and int(row[0]) >= 0 + assert row[1] is not None and int(row[1]) >= 0 + + +def test_calls_callee_declaring_role_matches_parent_symbol_role_yaml_brownfield( + tmp_path: Path, +) -> None: + """YAML role_overrides on declaring type → edge attr matches parent Symbol.role.""" + _load_brownfield_overrides.cache_clear() + collect_annotation_meta_chain.cache_clear() + root = tmp_path / "proj" + java_dir = root / "src/main/java/smoke" + java_dir.mkdir(parents=True) + (java_dir / "BrownfieldCallRole.java").write_text( + """ + package smoke; + + @interface LegacyServiceMarker { } + + @LegacyServiceMarker + class ConfigOnlyService { + void handle() { } + } + + class Caller { + void run(ConfigOnlyService svc) { + svc.handle(); + } + } + """.strip() + + "\n", + encoding="utf-8", + ) + (root / ".java-codebase-rag.yml").write_text( + "role_overrides:\n" + " annotations:\n" + " LegacyServiceMarker: SERVICE\n", + encoding="utf-8", + ) + db_path = build_kuzu_to(root, tmp_path / "g.kuzu", max_pass=3) + conn = _connect(db_path) + mismatches = _scalar( + conn, + "MATCH ()-[c:CALLS]->(dst:Symbol) " + "MATCH (parent:Symbol {id: dst.parent_id}) " + "WHERE c.callee_declaring_role <> parent.role " + "RETURN count(*)", + ) + assert mismatches == 0 + roles = _column( + conn, + "MATCH ()-[c:CALLS]->(dst:Symbol) " + "MATCH (parent:Symbol {id: dst.parent_id}) " + "WHERE parent.fqn = 'smoke.ConfigOnlyService' " + "RETURN DISTINCT c.callee_declaring_role", + ) + assert roles == ["SERVICE"] + + +def test_pass3_callee_declaring_role_bank_annotated_types(kuzu_db_path: Path) -> None: + """CALLS to methods on @Service declaring types carry callee_declaring_role=SERVICE.""" + conn = _connect(kuzu_db_path) + rows = _column( + conn, + "MATCH (src:Symbol)-[c:CALLS]->(dst:Symbol) " + "MATCH (parent:Symbol {id: dst.parent_id}) " + "WHERE 'Service' IN parent.annotations AND parent.role = 'SERVICE' " + "RETURN c.callee_declaring_role LIMIT 20", + ) + assert rows, "expected CALLS to @Service-declared callees on bank-chat-system" + assert all(str(r) == "SERVICE" for r in rows), rows + repo_rows = _column( + conn, + "MATCH (src:Symbol)-[c:CALLS]->(dst:Symbol) " + "MATCH (parent:Symbol {id: dst.parent_id}) " + "WHERE 'Repository' IN parent.annotations " + "RETURN DISTINCT c.callee_declaring_role", + ) + if repo_rows: + assert all(str(r) == "REPOSITORY" for r in repo_rows), repo_rows + + def test_graph_meta_present_and_versioned(kuzu_db_path: Path) -> None: conn = _connect(kuzu_db_path) r = conn.execute( diff --git a/tests/test_call_graph_smoke_roundtrip.py b/tests/test_call_graph_smoke_roundtrip.py index df6877d..03090cd 100644 --- a/tests/test_call_graph_smoke_roundtrip.py +++ b/tests/test_call_graph_smoke_roundtrip.py @@ -96,6 +96,38 @@ def test_wildcard_static_import_strategy(kuzu_db_path_call_graph_smoke: Path) -> assert "static_import_wildcard" in strats, strats +def test_pass3_supertype_dedup_jpa_repository_save_one_row( + kuzu_db_path_call_graph_smoke: Path, +) -> None: + """SupertypeDedupPatterns: interface + concrete save → one CALLS row, REPOSITORY role.""" + db = kuzu_db_path_call_graph_smoke + conn = _connect(db) + rows = _rows( + conn, + "MATCH (src:Symbol)-[c:CALLS]->(dst:Symbol) " + "WHERE src.fqn STARTS WITH 'smoke.SupertypeDedupPatterns#persist' " + "AND dst.name = 'save' " + "RETURN count(*) AS n, c.callee_declaring_role AS role", + ) + assert rows, "expected save call from SupertypeDedupPatterns#persist" + assert int(rows[0][0]) == 1, rows + assert str(rows[0][1]) == "REPOSITORY", rows + + +def test_pass3_overload_ambiguous_still_n_rows(kuzu_db_path_call_graph_smoke: Path) -> None: + """overload_ambiguous sites keep N rows after supertype dedup (OverloadPatterns#sameArity).""" + db = kuzu_db_path_call_graph_smoke + conn = _connect(db) + rows = _rows( + conn, + "MATCH (src:Symbol)-[c:CALLS]->(dst:Symbol) " + "WHERE src.fqn STARTS WITH 'smoke.OverloadPatterns#sameArity' " + "AND dst.name = 'amb' AND c.strategy = 'overload_ambiguous' " + "RETURN dst.fqn AS fqn", + ) + assert len(rows) == 2, f"expected 2 overload_ambiguous targets, got {rows}" + + def test_overload_sameArity_emits_two_overload_ambiguous_edges(kuzu_db_path_call_graph_smoke: Path) -> None: """§7.1 #13: two one-arg overloads → two resolved edges tagged overload_ambiguous.""" db = kuzu_db_path_call_graph_smoke diff --git a/tests/test_kuzu_queries.py b/tests/test_kuzu_queries.py index 5e31128..e38db77 100644 --- a/tests/test_kuzu_queries.py +++ b/tests/test_kuzu_queries.py @@ -385,7 +385,7 @@ def _open_stale_ontology_graph(tmp_path: Path, ontology_version: int) -> Path: def test_kuzu_graph_refuses_ontology_version_below_required(tmp_path: Path) -> None: - """v13 graphs refuse to open when ONTOLOGY_VERSION is 14 (SCHEMA-V2 PR-A). + """v13 graphs refuse to open when ``ONTOLOGY_VERSION`` is current (e.g. 15). Overlaps ``test_kuzu_graph_get_raises_when_graph_ontology_too_old`` when ``ONTOLOGY_VERSION - 1 == 13``; kept as an explicit v13 regression anchor. @@ -398,7 +398,11 @@ def test_kuzu_graph_refuses_ontology_version_below_required(tmp_path: Path) -> N try: KuzuGraph._instance = None KuzuGraph._instance_path = None - with pytest.raises(RuntimeError, match="(?i)ontology.*14|required version 14"): + ver = ONTOLOGY_VERSION + with pytest.raises( + RuntimeError, + match=rf"(?i)ontology.*{ver}|required version {ver}", + ): KuzuGraph.get(str(db_path)) finally: KuzuGraph._instance = prev_inst diff --git a/tests/test_schema_consistency.py b/tests/test_schema_consistency.py index 4bae2cd..2f30413 100644 --- a/tests/test_schema_consistency.py +++ b/tests/test_schema_consistency.py @@ -87,6 +87,17 @@ def test_http_async_typical_traversals_post_flip() -> None: assert "DECLARES_PRODUCER" in async_trav["member_subject"] +def test_edge_schema_calls_registers_callee_declaring_role() -> None: + attrs = {a.name for a in EDGE_SCHEMA["CALLS"].attrs} + assert "callee_declaring_role" in attrs + + +def test_calls_edge_has_callee_declaring_role_column() -> None: + text = _BUILD_AST_GRAPH.read_text(encoding="utf-8") + assert "callee_declaring_role STRING" in text + assert "callee_declaring_role: $callee_declaring_role" in text + + def test_brownfield_resolver_strategy_literals_emitted_in_builder_subset() -> None: literals = _strategy_literals_in_emitters() assert literals, "expected strategy literals from emitter modules"