diff --git a/docs/invariant/ai_evidence_and_provenance.md b/docs/invariant/ai_evidence_and_provenance.md index 5853781..88c1ea8 100644 --- a/docs/invariant/ai_evidence_and_provenance.md +++ b/docs/invariant/ai_evidence_and_provenance.md @@ -127,7 +127,7 @@ Benchmark material containing family records remains private application data un ## 6. Ownership and Change Policy -1. Canonical V4 architecture, schema, requirements, and error-policy documents define how current behavior satisfies this invariant. +1. Canonical V6.1 architecture, schema, requirements, and error-policy documents define how current behavior satisfies this invariant. 2. Provider adapters own the capture of provider-boundary evidence. 3. Services own validation, persistence, retention, and export behavior. 4. UI pages may inspect evidence through service contracts but do not define evidence semantics. diff --git a/tests/test_meta_contract_guards.py b/tests/test_meta_contract_guards.py index f79ba3b..3974f6b 100644 --- a/tests/test_meta_contract_guards.py +++ b/tests/test_meta_contract_guards.py @@ -64,7 +64,10 @@ BASELINE_SCAN_EXCLUSIONS = frozenset( ) _BASELINE_DECLARATION = re.compile(r"Current Baseline:\s*V(\d+\.\d+)") -_CURRENT_VERSION_CLAIM = re.compile(r"\b(?:current|active)\s+V(\d+\.\d+)", re.IGNORECASE) +_BASELINE_CLAIM = re.compile( + r"\b(?:Current Baseline:\s*|current\s+|active\s+|canonical\s+)V(\d+(?:\.\d+)?)", + re.IGNORECASE, +) def _declared_baseline() -> str: @@ -98,12 +101,7 @@ def test_canonical_docs_declare_one_consistent_baseline(): for path in _baseline_scanned_docs(): relative = path.relative_to(PROJECT_ROOT).as_posix() text = path.read_text(encoding="utf-8") - stale = { - version - for pattern in (_BASELINE_DECLARATION, _CURRENT_VERSION_CLAIM) - for version in pattern.findall(text) - if version != baseline - } + stale = {version for version in _BASELINE_CLAIM.findall(text) if version != baseline} if stale: violations[relative] = sorted(stale) diff --git a/tests/test_orphan_sweep.py b/tests/test_orphan_sweep.py index 0035a47..10c5b9e 100644 --- a/tests/test_orphan_sweep.py +++ b/tests/test_orphan_sweep.py @@ -22,6 +22,7 @@ SOURCE_DIR = PROJECT_ROOT / "src" / "transcription" # Every tree that may legitimately consume package API. REFERENCE_ROOTS = (SOURCE_DIR, PROJECT_ROOT / "tests", PROJECT_ROOT / "tools") +MODULE_REFERENCE_ROOTS = (SOURCE_DIR, PROJECT_ROOT / "tools") # Decorators that hand a callable to a framework registry, making the definition # reachable without any in-repo reference to its name. @@ -31,62 +32,70 @@ REGISTRATION_DECORATOR_PREFIXES = ("router.", "app.", "ui.page") # rationale. Removing the code means removing the entry; adding an entry means an # explicit decision to keep unreferenced code. KNOWN_ORPHANS: dict[str, str] = { - "DocumentService.is_document_type_referenced": ( + "src/transcription/services/documents.py::DocumentService.is_document_type_referenced": ( "Read helper currently bypassed by callers in favor of direct delete guards. " "Retained for now to preserve service API stability during cleanup." ), - "DocumentService.is_tag_referenced": ( + "src/transcription/services/documents.py::DocumentService.is_tag_referenced": ( "Read helper currently bypassed by callers in favor of direct delete guards. " "Retained for now to preserve service API stability during cleanup." ), - "DocumentService.read_document": ( + "src/transcription/services/documents.py::DocumentService.read_document": ( "Public CRUD read method currently unused by runtime routes, but retained as " "part of the service API shape for downstream call sites." ), - "DocumentService.read_document_type": ( + "src/transcription/services/documents.py::DocumentService.read_document_type": ( "Public CRUD read method currently unused by runtime routes, but retained as " "part of the service API shape for downstream call sites." ), - "DocumentService.read_tag": ( + "src/transcription/services/documents.py::DocumentService.read_tag": ( "Public CRUD read method currently unused by runtime routes, but retained as " "part of the service API shape for downstream call sites." ), - "JSONBCompat.load_dialect_impl": ( - "SQLAlchemy type hook is invoked by ORM internals via subclass protocol rather " - "than direct in-repo references; keep this implementation method." - ), - "JobService.delete_job": ( + "src/transcription/services/jobs.py::JobService.delete_job": ( "Legacy delete method retained as a compatibility shim while guarded deletion " "flows migrate fully to delete_job_with_guardrails." ), - "JobService.update_job": ( + "src/transcription/services/jobs.py::JobService.update_job": ( "Legacy update method retained for compatibility while route and workflow " "callers continue converging on narrower state-transition APIs." ), - "PeopleService.is_person_role_referenced": ( + "src/transcription/services/people.py::PeopleService.is_person_role_referenced": ( "Reference-check helper currently not called by route workflows, but kept with " "the service surface while role-management cleanup remains in progress." ), - "PeopleService.read_person": ( + "src/transcription/services/people.py::PeopleService.read_person": ( "Public CRUD read method currently unused by runtime routes, but retained as " "part of the service API shape for downstream call sites." ), - "PeopleService.read_person_role": ( + "src/transcription/services/people.py::PeopleService.read_person_role": ( "Public CRUD read method currently unused by runtime routes, but retained as " "part of the service API shape for downstream call sites." ), - "Settings.normalize_provider_models": ( + "src/transcription/config.py::Settings.normalize_provider_models": ( "Pydantic model-level validator is executed by framework hooks using decorator " "registration and therefore has no direct symbolic call site." ), - "Settings.validate_provider_models_input": ( + "src/transcription/config.py::Settings.validate_provider_models_input": ( "Pydantic field validator is executed by framework hooks using decorator " "registration and therefore has no direct symbolic call site." ), - "SourceService.read_job_source_for_job": ( + "src/transcription/benchmarking.py": ( + "Retained as the evaluation-policy implementation for scoring preserved execution " + "attempts, even though application runtime paths do not import it directly." + ), + "src/transcription/db/models.py::JSONBCompat.load_dialect_impl": ( + "SQLAlchemy type hook is invoked by ORM internals via subclass protocol rather " + "than direct in-repo references; keep this implementation method." + ), + "src/transcription/services/sources.py::SourceService.read_job_source_for_job": ( "Public read helper currently unused by runtime flows, but retained as part of " "the SourceService API pending endpoint consolidation." ), + "src/transcription/ui/pages/tags_page.py": ( + "Retained temporarily as explicitly dead code until the planned route-retirement " + "cleanup deletes the stranded module." + ), } @@ -94,6 +103,22 @@ def _source_files() -> list[Path]: return sorted(SOURCE_DIR.rglob("*.py")) +def _entrypoint_modules() -> set[str]: + return { + "src/transcription/app.py", + "src/transcription/__main__.py", + "src/transcription/worker_service.py", + } + + +def _module_key(path: Path) -> str: + return path.relative_to(PROJECT_ROOT).as_posix() + + +def _leaf_definition_name(qualified_name: str) -> str: + return qualified_name.split("::", 1)[-1].split(".")[-1] + + def _is_registered_with_framework(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> bool: return any(ast.unparse(decorator).startswith(REGISTRATION_DECORATOR_PREFIXES) for decorator in node.decorator_list) @@ -108,16 +133,15 @@ def _public_definitions() -> dict[str, str]: continue if node.name.startswith("_") or _is_registered_with_framework(node): continue - definitions[node.name] = f"{path.relative_to(PROJECT_ROOT).as_posix()}:{node.lineno}" + module = _module_key(path) + definitions[f"{module}::{node.name}"] = f"{module}:{node.lineno}" if isinstance(node, ast.ClassDef): for method in node.body: if not isinstance(method, ast.FunctionDef | ast.AsyncFunctionDef): continue if method.name.startswith("_"): continue - definitions[f"{node.name}.{method.name}"] = ( - f"{path.relative_to(PROJECT_ROOT).as_posix()}:{method.lineno}" - ) + definitions[f"{module}::{node.name}.{method.name}"] = f"{module}:{method.lineno}" return definitions @@ -146,19 +170,72 @@ def _referenced_names() -> tuple[set[str], str]: return names, "\n".join(literals) +def _imported_source_modules() -> set[str]: + imported: set[str] = set() + + def _mark_module_and_packages(module_path: Path) -> None: + imported.add(_module_key(module_path)) + for parent in module_path.parents: + package_init = parent / "__init__.py" + if package_init.exists() and package_init.is_relative_to(SOURCE_DIR): + imported.add(_module_key(package_init)) + + for root in MODULE_REFERENCE_ROOTS: + for path in sorted(root.rglob("*.py")): + tree = ast.parse(path.read_text(encoding="utf-8")) + for node in ast.walk(tree): + if not isinstance(node, ast.ImportFrom) or not node.module: + continue + if node.level > 0: + anchor = path.parent + for _ in range(node.level - 1): + anchor = anchor.parent + target = anchor / Path(*node.module.split(".")) + file_candidate = target.with_suffix(".py") + package_candidate = target / "__init__.py" + if file_candidate.exists(): + _mark_module_and_packages(file_candidate) + elif package_candidate.exists(): + _mark_module_and_packages(package_candidate) + continue + module_path = Path(*node.module.split(".")) + if not module_path.parts or module_path.parts[0] != "transcription": + continue + target = SOURCE_DIR / Path(*module_path.parts[1:]) + file_candidate = target.with_suffix(".py") + package_candidate = target / "__init__.py" + if file_candidate.exists(): + _mark_module_and_packages(file_candidate) + elif package_candidate.exists(): + _mark_module_and_packages(package_candidate) + return imported + + +def _orphan_modules() -> dict[str, str]: + imported = _imported_source_modules() + entrypoints = _entrypoint_modules() + return { + module: module + for path in _source_files() + if (module := _module_key(path)) not in imported and module not in entrypoints + } + + def _orphans() -> dict[str, str]: definitions = _public_definitions() names, literal_blob = _referenced_names() + orphan_modules = set(_orphan_modules()) return { name: location for name, location in definitions.items() + if name.split("::", 1)[0] not in orphan_modules # A definition is referenced if its name is used directly, or appears inside a # string such as "transcription.__main__:create_cli_app". if ( - name not in names - and name not in literal_blob - and name.split(".")[-1] not in names - and name.split(".")[-1] not in literal_blob + name not in literal_blob + and name.split("::", 1)[-1] not in literal_blob + and _leaf_definition_name(name) not in names + and _leaf_definition_name(name) not in literal_blob ) } @@ -166,8 +243,8 @@ def _orphans() -> dict[str, str]: def test_public_definitions_are_discovered(): """Guard the guard: the sweep is meaningless if nothing is scanned.""" definitions = _public_definitions() - assert len(definitions) >= 420 - assert "create_app" in definitions + assert "src/transcription/app.py::create_app" in definitions + assert "src/transcription/services/documents.py::DocumentService.create_document" in definitions def test_framework_registered_routes_are_exempt(): @@ -183,9 +260,15 @@ def test_no_unexpected_orphaned_definitions(): assert unexpected == {} +def test_no_unexpected_orphaned_modules(): + """Dead modules must be removed or recorded explicitly with a rationale.""" + unexpected = {name: location for name, location in _orphan_modules().items() if name not in KNOWN_ORPHANS} + assert unexpected == {} + + def test_known_orphans_are_still_orphaned(): """Keep the allowlist honest: an entry that regained callers must be removed.""" - current = set(_orphans()) + current = set(_orphans()) | set(_orphan_modules()) stale = sorted(name for name in KNOWN_ORPHANS if name not in current) assert stale == [], "these definitions are referenced again; drop them from KNOWN_ORPHANS"