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

25 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-02 20:26 +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 

17import fnmatch 

18from typing import Any 

19 

20 

21def _matches_any(path: str, globs: tuple[str, ...]) -> bool: 

22 for g in globs: 

23 if fnmatch.fnmatch(path, g): 

24 return True 

25 return False 

26 

27 

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

29 

30 

31def verify( 

32 declared_files: list[str] | None, 

33 actual_files: list[str], 

34 *, 

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

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

37) -> dict[str, Any]: 

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

39 

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

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

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

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

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

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

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

47 

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

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

50 that carried creep or had no declared scope. 

51 """ 

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

53 if declared_files is None: 

54 return { 

55 "schema_version": SCHEMA_VERSION, 

56 "status": "pass", 

57 "advisory": True, 

58 "waived": waived, 

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

60 "declared": None, 

61 "in_scope": [], 

62 "scope_creep": [], 

63 "docs_exempt": [], 

64 } 

65 declared = set(declared_files) 

66 in_scope: list[str] = [] 

67 creep: list[str] = [] 

68 docs_exempt: list[str] = [] 

69 for path in actual_files: 

70 if path in declared: 

71 in_scope.append(path) 

72 elif docs_globs and _matches_any(path, docs_globs): 

73 docs_exempt.append(path) 

74 else: 

75 creep.append(path) 

76 blocking = bool(creep) and not waived 

77 return { 

78 "schema_version": SCHEMA_VERSION, 

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

80 "advisory": False, 

81 "waived": waived, 

82 "note": ("scope creep waived by operator deferral" if creep and waived else None), 

83 "declared": sorted(declared), 

84 "in_scope": in_scope, 

85 "scope_creep": creep, 

86 "docs_exempt": docs_exempt, 

87 }