"""The CAP/ICCR endometrium checklist field list: the single source of truth. Generated by scripts/generate_fields.py from the confirmed field-to-concept mapping (endopath.mapping.cap_checklist_coverage, #95) and the taxonomy registry (endopath.taxonomy, #35). Do not edit by hand: run `PYTHONPATH=src python3 scripts/generate_fields.py` and commit the result. tests/test_field_generation.py fails when this file drifts from a fresh render. Every consumer derives from `FIELDS` below: schema.py re-exports the enums, llm_extraction reads FIELD_VALUE_SCHEMAS and ENUM_FIELDS, colpali_retrieval reads FIELD_QUERIES, and the frontend renders the checklist from GET /api/fields (issue #37). **Standard library only, deliberately.** `scripts/modal_precompute.py` runs on a Modal image carrying only colpali-engine/transformers/torch/pillow/numpy, so it cannot import pydantic. Its `@app.local_entrypoint` imports `FIELD_QUERIES` from here in the local process and passes it to the GPU function as an argument. A test asserts this module stays import-clean; adding a third-party import here silently breaks that job. (Note `endopath/__init__.py` imports schema, so the Modal container must never import `endopath.fields` directly either.) The value enums live here rather than in schema.py because a field's permitted values are part of its definition, and schema.py's Pydantic models sit on top of that. schema.py re-exports them, so `from endopath.schema import Histotype` still resolves. Field *order* is meaningful: it is the order Case Review renders evidence cards. Several value sets differ from how docs/taxonomy.md and the registry model the same concept. Those differences are annotated on the fields below and reconciled separately: #81 (histotype, LVSI, myometrial, cervical, nodal, and molecular value sets), #62 (pathologic_stage as a reported field plus per-edition computed views), and #36 (the dedifferentiated/undifferentiated split). """ from __future__ import annotations from dataclasses import dataclass from enum import Enum from typing import Any, Optional class Histotype(str, Enum): ENDOMETRIOID = "endometrioid" MUCINOUS = "mucinous" SEROUS = "serous" CLEAR_CELL = "clear_cell" CARCINOSARCOMA = "carcinosarcoma" DEDIFFERENTIATED_UNDIFFERENTIATED = "dedifferentiated_undifferentiated" MIXED = "mixed" OTHER_NOS = "other_nos" class LvsiStatus(str, Enum): NOT_IDENTIFIED = "not_identified" FOCAL = "focal" SUBSTANTIAL = "substantial" class MolecularClassification(str, Enum): POLE_ULTRAMUTATED = "pole_ultramutated" MMR_DEFICIENT = "mmr_deficient" P53_ABNORMAL = "p53_abnormal" NO_SPECIFIC_MOLECULAR_PROFILE = "no_specific_molecular_profile" MULTIPLE_CLASSIFIER = "multiple_classifier" def _enum_schema(enum_cls: type[Enum]) -> dict[str, Any]: return {"type": "string", "enum": [member.value for member in enum_cls]} @dataclass(frozen=True) class FieldSpec: """One checklist field, in every form the codebase needs it. `query` is the ColPali retrieval phrase, and the key under which that phrase's precomputed vector is stored in data/embeddings/query_embeddings.npz. Renaming `name` therefore invalidates that file; `tests/test_fields.py` asserts the two agree so the break is loud rather than silent. `value_schema` is JSON Schema, consumed directly as the LLM tool-use parameter schema (llm_extraction.CHECKLIST_TOOL) and surfaced over /api/fields so the frontend can render a dropdown instead of a text box. `python_enum` is set only where the value coerces back to a Python Enum. """ name: str label: str query: str value_schema: dict[str, Any] python_enum: Optional[type[Enum]] = None @property def value_type(self) -> str: return self.value_schema["type"] @property def enum_values(self) -> Optional[list[str]]: return self.value_schema.get("enum") FIELDS: tuple[FieldSpec, ...] = ( # Registry `histotype` splits dedifferentiated/undifferentiated, drops mucinous, and names # the residual other_not_listed (not other_nos): #81, #36. FieldSpec( name="histologic_type", label="Histologic type", query="histologic type", value_schema=_enum_schema(Histotype), python_enum=Histotype, ), # Registry `figo_grade` also carries the off-branch high_grade_non_endometrioid value; the # CAP field presents only FIGO 1/2/3: #81. FieldSpec( name="histologic_grade", label="Histologic grade (FIGO)", query="FIGO grade", value_schema={"type": "string", "enum": ["1", "2", "3"]}, ), # Registry stores invasion_depth and myometrial_thickness in mm and derives the percent # (numbers stay numbers); the CAP field stores the percent directly: #81, #38. FieldSpec( name="myometrial_invasion_percent", label="Myometrial invasion %", query="myometrial invasion depth", value_schema={"type": "number", "minimum": 0, "maximum": 100}, ), # Registry models cervical_stromal_invasion as coded present/absent/cannot_be_assessed; the # CAP field is a boolean: #81. FieldSpec( name="cervical_stromal_invasion", label="Cervical stromal invasion", query="cervical stromal invasion", value_schema={"type": "boolean"}, ), # Registry separates asserted lvsi_status and foci count from derived lvsi_extent # (negative/focal/substantial); the CAP field stores the category with not_identified for # negative: #81. FieldSpec( name="lymphovascular_space_invasion", label="LVSI", query="lymphovascular space invasion", value_schema=_enum_schema(LvsiStatus), python_enum=LvsiStatus, ), # Registry models pn_category as a coded value set (pN0..pN2a); the CAP field is an open # string: #81. FieldSpec( name="regional_lymph_node_status", label="Regional lymph node status", query="regional lymph node status", value_schema={"type": "string"}, ), # Registry derives figo_stage per FIGO edition (derive_stage); the CAP field is one reported # string. Split into a reported field plus computed views: #62. FieldSpec( name="pathologic_stage", label="Pathologic stage", query="pathologic stage", value_schema={"type": "string"}, ), # Registry keeps promise_class and tcga_class as two dimensions; the CAP field mixes TCGA # and ProMisE naming in one enum: #81. FieldSpec( name="molecular_classification", label="Molecular classification", query="molecular classification", value_schema=_enum_schema(MolecularClassification), python_enum=MolecularClassification, ), ) BY_NAME: dict[str, FieldSpec] = {spec.name: spec for spec in FIELDS} FIELD_NAMES: tuple[str, ...] = tuple(spec.name for spec in FIELDS) FIELD_LABELS: dict[str, str] = {spec.name: spec.label for spec in FIELDS} FIELD_QUERIES: dict[str, str] = {spec.name: spec.query for spec in FIELDS} FIELD_VALUE_SCHEMAS: dict[str, dict[str, Any]] = {spec.name: spec.value_schema for spec in FIELDS} ENUM_FIELDS: dict[str, type[Enum]] = { spec.name: spec.python_enum for spec in FIELDS if spec.python_enum is not None }