Coverage for src/pullapprove/checklists.py: 66%
38 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-01-26 14:10 -0600
« prev ^ index » next coverage.py v7.8.2, created at 2026-01-26 14:10 -0600
1"""
2Configuration models for approval checklists.
4A simpler alternative to forms for compliance sign-off before approval.
5Each item is a checkbox with configurable requirement and notes settings.
6"""
8from __future__ import annotations
10import hashlib
11import json
12from enum import Enum
13from typing import Any
15from pydantic import (
16 BaseModel,
17 ConfigDict,
18 Field,
19)
22class NotesOption(str, Enum):
23 """Controls when/if notes are shown/required for a checklist item."""
25 DISABLED = "disabled" # No notes field
26 OPTIONAL = "optional" # Notes field available but not required
27 REQUIRED = "required" # Notes always required
28 REQUIRED_IF_CHECKED = "required_if_checked" # Notes required when item is checked
29 REQUIRED_IF_UNCHECKED = "required_if_unchecked" # Notes required when not checked
32class ChecklistItem(BaseModel):
33 """A single checklist item.
35 Settings:
36 - required: Whether the item must be checked (default: True)
37 - notes: Controls when/if notes are shown/required (default: "optional")
38 - help: Optional help text displayed below the label
40 Notes options:
41 - "disabled": No notes field
42 - "optional": Notes field available but not required (default)
43 - "required": Notes always required
44 - "required_if_checked": Notes required when item is checked
45 - "required_if_unchecked": Notes required when item is not checked
46 """
48 model_config = ConfigDict(extra="forbid")
50 label: str = Field(min_length=1)
51 required: bool = True
52 notes: NotesOption = NotesOption.OPTIONAL
53 help: str = ""
55 # Runtime values (not part of config, populated during submission)
56 checked: bool = False
57 notes_value: str = ""
59 def notes_required_for_state(self, checked: bool) -> bool:
60 """Check if notes are required given the checked state."""
61 if self.notes == NotesOption.REQUIRED:
62 return True
63 if self.notes == NotesOption.REQUIRED_IF_CHECKED and checked:
64 return True
65 if self.notes == NotesOption.REQUIRED_IF_UNCHECKED and not checked:
66 return True
67 return False
70class Checklist(BaseModel):
71 """Configuration for approval checklists."""
73 model_config = ConfigDict(extra="forbid")
75 items: list[ChecklistItem] = Field(min_length=1)
77 def compute_hash(self) -> str:
78 """Compute deterministic hash of checklist definition.
80 Only includes semantic attributes that define the checklist:
81 - label, required, notes, help
82 Excludes runtime values:
83 - checked, notes_value
84 """
85 serialized: list[dict[str, Any]] = []
86 for item in self.items:
87 semantic: dict[str, Any] = {
88 "label": item.label,
89 "required": item.required,
90 "notes": item.notes.value,
91 "help": item.help,
92 }
93 serialized.append(semantic)
94 canonical = json.dumps(serialized, sort_keys=True)
95 return hashlib.sha256(canonical.encode()).hexdigest()