Coverage for src/keel/scope.py: 100%

20 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-16 18:07 +0000

1"""Branch-scope verification — declared files vs. the observed PR diff. 

2 

3keel's adapter prose calls the implementer's declared-files-vs-actual-diff 

4comparison "the primary defence against branch contamination". This module makes 

5that defence enforceable: given the implementer's *declared* file set, the live 

6PR diff's changed files, and the project's docs-gate globs, it computes which 

7diff files fall outside the declared scope ("scope creep") and returns a verdict. 

8 

9Pure and deterministic: every input is data and there is no I/O, so the verdict 

10is a function of its arguments alone (the cli loads the ledger record and the 

11live diff). Docs-path matching reuses the same glob matcher as risk 

12classification so docs-only extras are exempt rather than flagged. 

13""" 

14 

15from __future__ import annotations 

16 

17from typing import Any 

18 

19from .classify import _matches_any 

20 

21SCHEMA_VERSION = "keel.scope-verify.v1" 

22 

23 

24def verify( 

25 declared_files: list[str] | None, 

26 actual_files: list[str], 

27 *, 

28 docs_globs: tuple[str, ...] = (), 

29 deferrals: tuple[str, ...] = (), 

30) -> dict[str, Any]: 

31 """Compare ``declared_files`` against ``actual_files`` and return a verdict. 

32 

33 * ``declared_files`` is the implementer's recorded scope contract, or 

34 ``None`` when no scope was recorded. With ``None`` the result is an 

35 advisory pass carrying a ``no-declared-scope`` note — back-compat so 

36 existing flows that never recorded a scope are never broken. 

37 * Files in ``actual_files`` not present in ``declared_files`` are scope 

38 creep, *unless* they match ``docs_globs`` (docs extras are allowed) or the 

39 operator has waived scope via a ``scope-waived`` deferral. 

40 

41 The returned report lists the in-scope files and the creep files and sets a 

42 ``pass``/``fail`` status. ``waived`` and ``advisory`` flags explain a pass 

43 that carried creep or had no declared scope. 

44 """ 

45 waived = "scope-waived" in deferrals or "all" in deferrals 

46 if declared_files is None: 

47 return { 

48 "schema_version": SCHEMA_VERSION, 

49 "status": "pass", 

50 "advisory": True, 

51 "waived": waived, 

52 "note": "no declared scope recorded", 

53 "declared": None, 

54 "in_scope": [], 

55 "scope_creep": [], 

56 "docs_exempt": [], 

57 } 

58 declared = set(declared_files) 

59 in_scope: list[str] = [] 

60 creep: list[str] = [] 

61 docs_exempt: list[str] = [] 

62 for path in actual_files: 

63 if path in declared: 

64 in_scope.append(path) 

65 elif docs_globs and _matches_any(path, docs_globs): 

66 docs_exempt.append(path) 

67 else: 

68 creep.append(path) 

69 blocking = bool(creep) and not waived 

70 return { 

71 "schema_version": SCHEMA_VERSION, 

72 "status": "fail" if blocking else "pass", 

73 "advisory": False, 

74 "waived": waived, 

75 "note": ( 

76 "scope creep waived by operator deferral" 

77 if creep and waived 

78 else None 

79 ), 

80 "declared": sorted(declared), 

81 "in_scope": in_scope, 

82 "scope_creep": creep, 

83 "docs_exempt": docs_exempt, 

84 }