skillpool 4.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skillpool/__init__.py +74 -0
- skillpool/__main__.py +6 -0
- skillpool/adapters/__init__.py +8 -0
- skillpool/adapters/base.py +41 -0
- skillpool/adapters/claude_adapter.py +36 -0
- skillpool/adapters/codex_adapter.py +92 -0
- skillpool/adapters/hermes_adapter.py +38 -0
- skillpool/audit/__init__.py +651 -0
- skillpool/bridge/__init__.py +16 -0
- skillpool/bridge/freeze_detector.py +134 -0
- skillpool/bridge/maintenance.py +119 -0
- skillpool/bridge/wal_manager.py +136 -0
- skillpool/clawmem_client.py +176 -0
- skillpool/cli.py +700 -0
- skillpool/combiner/__init__.py +31 -0
- skillpool/combiner/lifecycle.py +453 -0
- skillpool/combiner/models.py +99 -0
- skillpool/config.py +34 -0
- skillpool/cost/__init__.py +111 -0
- skillpool/cost/audit_hash.py +51 -0
- skillpool/cost/budget_tracker.py +66 -0
- skillpool/cost/dashboard.py +189 -0
- skillpool/cost/models.py +129 -0
- skillpool/cost/token_governor.py +264 -0
- skillpool/cost/trace_ceiling.py +38 -0
- skillpool/csdf.py +126 -0
- skillpool/evolver/__init__.py +978 -0
- skillpool/gain/__init__.py +285 -0
- skillpool/gate.py +282 -0
- skillpool/gate_policy/__init__.py +31 -0
- skillpool/gate_policy/incremental.py +157 -0
- skillpool/gate_policy/parser.py +258 -0
- skillpool/gate_policy/state_machine.py +432 -0
- skillpool/graph/__init__.py +14 -0
- skillpool/graph/ppr.py +279 -0
- skillpool/health/__init__.py +73 -0
- skillpool/health/check.py +85 -0
- skillpool/health/degradation.py +90 -0
- skillpool/health/models.py +43 -0
- skillpool/hooks/__init__.py +4 -0
- skillpool/hooks/security_scanner.py +288 -0
- skillpool/lifecycle.py +150 -0
- skillpool/materializer/__init__.py +124 -0
- skillpool/materializer/budget_cropper.py +178 -0
- skillpool/materializer/csdf_loader.py +114 -0
- skillpool/materializer/lazy_loader.py +265 -0
- skillpool/materializer/lifecycle_filter.py +93 -0
- skillpool/materializer/mapper.py +178 -0
- skillpool/materializer/models.py +66 -0
- skillpool/mcp_server.py +2005 -0
- skillpool/monitor/__init__.py +576 -0
- skillpool/monitor/bug_collector.py +392 -0
- skillpool/monitor/defect_classifier.py +218 -0
- skillpool/monitor/self_healing.py +530 -0
- skillpool/monitor/telemetry_bridge.py +197 -0
- skillpool/paradigm/__init__.py +312 -0
- skillpool/paradigm/override.py +285 -0
- skillpool/profile.py +94 -0
- skillpool/quality.py +254 -0
- skillpool/registry/__init__.py +509 -0
- skillpool/registry/models.py +98 -0
- skillpool/resolver/__init__.py +320 -0
- skillpool/resolver/cache.py +103 -0
- skillpool/resolver/circuit_breaker.py +103 -0
- skillpool/resolver/conflict_detector.py +111 -0
- skillpool/resolver/health_filter.py +38 -0
- skillpool/resolver/models.py +154 -0
- skillpool/resolver/rate_limiter.py +48 -0
- skillpool/resolver/skill_graph.py +183 -0
- skillpool/review/__init__.py +242 -0
- skillpool/review/async_queue.py +96 -0
- skillpool/review/checkpoint_runner.py +345 -0
- skillpool/review/models.py +164 -0
- skillpool/review/suspect_marker.py +39 -0
- skillpool/review/veto_evaluator.py +94 -0
- skillpool/router/__init__.py +481 -0
- skillpool/schemas.py +119 -0
- skillpool/synergy/__init__.py +240 -0
- skillpool/synergy/detector.py +5 -0
- skillpool/telemetry.py +126 -0
- skillpool/utils/__init__.py +21 -0
- skillpool/utils/changelog.py +218 -0
- skillpool/utils/logger.py +273 -0
- skillpool/utils/runtime_audit.py +163 -0
- skillpool/utils/time_utils.py +13 -0
- skillpool-4.3.0.dist-info/METADATA +21 -0
- skillpool-4.3.0.dist-info/RECORD +90 -0
- skillpool-4.3.0.dist-info/WHEEL +5 -0
- skillpool-4.3.0.dist-info/entry_points.txt +3 -0
- skillpool-4.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Gate Policy Incremental Assessor — Detect changed files and assess complexity.
|
|
2
|
+
|
|
3
|
+
Error Codes:
|
|
4
|
+
GP005: git diff execution failure
|
|
5
|
+
|
|
6
|
+
Contracts:
|
|
7
|
+
- git diff failure returns empty list, never crashes (B18)
|
|
8
|
+
- Timeout enforced via subprocess timeout (B12)
|
|
9
|
+
- Per-file level resolution with highest-level aggregation
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from skillpool.gate_policy.parser import (
|
|
22
|
+
GatePolicyConfig,
|
|
23
|
+
resolve_level_for_path,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Models
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ComplexityAssessment(BaseModel):
|
|
35
|
+
"""Result of incremental complexity assessment."""
|
|
36
|
+
|
|
37
|
+
level: str | None = None
|
|
38
|
+
changed_files: list[str] = Field(default_factory=list)
|
|
39
|
+
per_file_levels: dict[str, str] = Field(default_factory=dict)
|
|
40
|
+
override_applied: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Level ordering
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
_LEVEL_ORDER = {"L0": 0, "L1": 1, "L2": 2, "L3+L2+": 3}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# IncrementalAssessor
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class IncrementalAssessor:
|
|
56
|
+
"""Detect changed files and assess per-file complexity.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
policy: GatePolicyConfig for path-based level resolution.
|
|
60
|
+
git_timeout: Max seconds for git diff command (default: 5).
|
|
61
|
+
|
|
62
|
+
Contract:
|
|
63
|
+
- git diff failure returns empty list, never crashes (B18).
|
|
64
|
+
- Timeout enforced via subprocess timeout (B12).
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
policy: GatePolicyConfig,
|
|
70
|
+
git_timeout: int = 5,
|
|
71
|
+
) -> None:
|
|
72
|
+
self._policy = policy
|
|
73
|
+
self._git_timeout = git_timeout
|
|
74
|
+
|
|
75
|
+
def detect_changed_files(
|
|
76
|
+
self,
|
|
77
|
+
base_ref: str = "HEAD",
|
|
78
|
+
cwd: Path | None = None,
|
|
79
|
+
) -> list[str]:
|
|
80
|
+
"""Detect files changed since base_ref.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
base_ref: Git ref to compare against (default: "HEAD").
|
|
84
|
+
cwd: Working directory for git command.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of relative file paths.
|
|
88
|
+
|
|
89
|
+
Contract:
|
|
90
|
+
- Uses `git diff --name-only <base_ref>`.
|
|
91
|
+
- base_ref validated: alphanumeric, dots, dashes, slashes only.
|
|
92
|
+
- Timeout: git_timeout seconds (B12).
|
|
93
|
+
- Fallback: empty list on any error (B18).
|
|
94
|
+
"""
|
|
95
|
+
# Validate base_ref to prevent injection
|
|
96
|
+
if not re.match(r"^[a-zA-Z0-9._/\-]+$", base_ref):
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
result = subprocess.run(
|
|
101
|
+
["git", "diff", "--name-only", base_ref],
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True,
|
|
104
|
+
timeout=self._git_timeout,
|
|
105
|
+
cwd=cwd,
|
|
106
|
+
)
|
|
107
|
+
if result.returncode != 0:
|
|
108
|
+
logger.warning("GP005: git diff returned non-zero: %s", result.stderr.strip())
|
|
109
|
+
return []
|
|
110
|
+
files = [f for f in result.stdout.strip().split("\n") if f]
|
|
111
|
+
return files
|
|
112
|
+
except subprocess.TimeoutExpired:
|
|
113
|
+
logger.warning("GP005: git diff timed out after %ds", self._git_timeout)
|
|
114
|
+
return []
|
|
115
|
+
except (FileNotFoundError, OSError) as e:
|
|
116
|
+
logger.warning("GP005: git diff execution failed: %s", e)
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
def assess_complexity(
|
|
120
|
+
self,
|
|
121
|
+
files: list[str],
|
|
122
|
+
) -> ComplexityAssessment:
|
|
123
|
+
"""Assess complexity for a list of changed files.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
files: List of relative file paths.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
ComplexityAssessment with level, per_file_levels, override_applied.
|
|
130
|
+
|
|
131
|
+
Contract:
|
|
132
|
+
- Empty files list → level=None (AC08).
|
|
133
|
+
- Per-file level via resolve_level_for_path().
|
|
134
|
+
- Aggregated level = highest across all files (AC10).
|
|
135
|
+
- skip_phases = union of all files' skip_phases.
|
|
136
|
+
"""
|
|
137
|
+
if not files:
|
|
138
|
+
return ComplexityAssessment(changed_files=[], per_file_levels={})
|
|
139
|
+
|
|
140
|
+
per_file: dict[str, str] = {}
|
|
141
|
+
highest_level = "L0"
|
|
142
|
+
override_applied: str | None = None
|
|
143
|
+
|
|
144
|
+
for f in files:
|
|
145
|
+
resolution = resolve_level_for_path(f, self._policy)
|
|
146
|
+
per_file[f] = resolution.level
|
|
147
|
+
if _LEVEL_ORDER.get(resolution.level, 0) > _LEVEL_ORDER.get(highest_level, 0):
|
|
148
|
+
highest_level = resolution.level
|
|
149
|
+
if resolution.matched_rules:
|
|
150
|
+
override_applied = resolution.matched_rules[0]
|
|
151
|
+
|
|
152
|
+
return ComplexityAssessment(
|
|
153
|
+
level=highest_level,
|
|
154
|
+
changed_files=files,
|
|
155
|
+
per_file_levels=per_file,
|
|
156
|
+
override_applied=override_applied,
|
|
157
|
+
)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Gate Policy Parser — Parse gate.policy YAML and resolve path-based overrides.
|
|
2
|
+
|
|
3
|
+
Error Codes:
|
|
4
|
+
GP001: gate.policy file not found
|
|
5
|
+
GP002: YAML parse or validation error
|
|
6
|
+
|
|
7
|
+
Contracts:
|
|
8
|
+
- load_gate_policy(): Parse YAML → frozen GatePolicyConfig
|
|
9
|
+
- resolve_level_for_path(): Path → LevelResolution with overrides applied
|
|
10
|
+
- Deterministic: same inputs → same outputs (B10)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from fnmatch import fnmatch
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from typing import Literal
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Error Handling
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GatePolicyError(Exception):
|
|
30
|
+
"""Base exception for gate policy errors.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
error_code: One of GP001-GP006
|
|
34
|
+
detail: Human-readable description
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, error_code: str, detail: str):
|
|
38
|
+
self.error_code = error_code
|
|
39
|
+
self.detail = detail
|
|
40
|
+
super().__init__(f"[{error_code}] {detail}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Pydantic Models (frozen for B11)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DirectoryOverride(BaseModel):
|
|
49
|
+
"""Single directory override rule."""
|
|
50
|
+
|
|
51
|
+
path: str
|
|
52
|
+
minimum_level: str | None = None
|
|
53
|
+
maximum_level: str | None = None
|
|
54
|
+
skip_phases: list[str] = Field(default_factory=list)
|
|
55
|
+
skip_all: bool = False
|
|
56
|
+
reason: str = ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FilePattern(BaseModel):
|
|
60
|
+
"""File pattern override rule."""
|
|
61
|
+
|
|
62
|
+
pattern: str
|
|
63
|
+
skip_phases: list[str] = Field(default_factory=list)
|
|
64
|
+
skip_all: bool = False
|
|
65
|
+
maximum_level: str | None = None
|
|
66
|
+
reason: str = ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PhaseGate(BaseModel):
|
|
70
|
+
"""Gate check rule for a phase transition."""
|
|
71
|
+
|
|
72
|
+
required_artifacts: list[str] = Field(default_factory=list)
|
|
73
|
+
validation: str = ""
|
|
74
|
+
level_condition: str | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class EmergencyBypass(BaseModel):
|
|
78
|
+
"""Emergency bypass configuration."""
|
|
79
|
+
|
|
80
|
+
enabled: bool = False
|
|
81
|
+
config_file: str = "emergency_overrides.json"
|
|
82
|
+
allowed_phases: list[str] = Field(default_factory=lambda: ["SDD", "TDD"])
|
|
83
|
+
max_duration_hours: int = 24
|
|
84
|
+
require_retrospective: bool = True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ReviewTrigger(BaseModel):
|
|
88
|
+
"""Review checkpoint trigger condition."""
|
|
89
|
+
|
|
90
|
+
condition: str
|
|
91
|
+
checkpoint: str = "L4"
|
|
92
|
+
required: bool = True
|
|
93
|
+
reason: str = ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class EnforcementConfig(BaseModel):
|
|
97
|
+
"""Gate enforcement mode configuration."""
|
|
98
|
+
|
|
99
|
+
mode: Literal["strict", "permissive", "disabled"] = "strict"
|
|
100
|
+
hook_integration: bool = True
|
|
101
|
+
log_all_transitions: bool = True
|
|
102
|
+
audit_trail: bool = True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class GatePolicyConfig(BaseModel):
|
|
106
|
+
"""Complete gate.policy configuration. Frozen after creation (B11)."""
|
|
107
|
+
|
|
108
|
+
model_config = {"frozen": True}
|
|
109
|
+
|
|
110
|
+
version: str = "1.0"
|
|
111
|
+
default_level: str = "L2"
|
|
112
|
+
phase_gates: dict[str, PhaseGate] = Field(default_factory=dict)
|
|
113
|
+
directory_overrides: list[DirectoryOverride] = Field(default_factory=list)
|
|
114
|
+
file_patterns: list[FilePattern] = Field(default_factory=list)
|
|
115
|
+
emergency_bypass: EmergencyBypass = Field(default_factory=EmergencyBypass)
|
|
116
|
+
review_triggers: list[ReviewTrigger] = Field(default_factory=list)
|
|
117
|
+
enforcement: EnforcementConfig = Field(default_factory=EnforcementConfig)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class LevelResolution(BaseModel):
|
|
121
|
+
"""Result of resolving complexity level for a path."""
|
|
122
|
+
|
|
123
|
+
level: str
|
|
124
|
+
skip_phases: list[str] = Field(default_factory=list)
|
|
125
|
+
skip_all: bool = False
|
|
126
|
+
matched_rules: list[str] = Field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Level ordering for min/max comparisons
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
_LEVEL_ORDER = {"L0": 0, "L1": 1, "L2": 2, "L3+L2+": 3}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _level_ge(a: str, b: str) -> bool:
|
|
137
|
+
"""Return True if level a >= level b."""
|
|
138
|
+
return _LEVEL_ORDER.get(a, 0) >= _LEVEL_ORDER.get(b, 0)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _max_level(a: str, b: str) -> str:
|
|
142
|
+
"""Return the higher of two levels."""
|
|
143
|
+
return a if _LEVEL_ORDER.get(a, 0) >= _LEVEL_ORDER.get(b, 0) else b
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _min_level(a: str, b: str) -> str:
|
|
147
|
+
"""Return the lower of two levels."""
|
|
148
|
+
return a if _LEVEL_ORDER.get(a, 0) <= _LEVEL_ORDER.get(b, 0) else b
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# Core Functions
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_gate_policy(policy_path: Path) -> GatePolicyConfig:
|
|
157
|
+
"""Parse gate.policy YAML file into validated config.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
policy_path: Path to gate.policy YAML file.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
GatePolicyConfig: Frozen, validated configuration object.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
GatePolicyError: GP001 if file not found.
|
|
167
|
+
GatePolicyError: GP002 if YAML parse or validation error.
|
|
168
|
+
|
|
169
|
+
Contract:
|
|
170
|
+
- Same path always returns structurally identical config (B10).
|
|
171
|
+
- Config is frozen after creation (B11).
|
|
172
|
+
- All 6 sections populated or use defaults.
|
|
173
|
+
"""
|
|
174
|
+
if not policy_path.exists():
|
|
175
|
+
raise GatePolicyError("GP001", f"gate.policy not found: {policy_path}")
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
raw = yaml.safe_load(policy_path.read_text())
|
|
179
|
+
if raw is None:
|
|
180
|
+
raw = {}
|
|
181
|
+
return GatePolicyConfig.model_validate(raw)
|
|
182
|
+
except yaml.YAMLError as e:
|
|
183
|
+
raise GatePolicyError("GP002", f"YAML parse error: {e}") from e
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise GatePolicyError("GP002", f"Validation error: {e}") from e
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def resolve_level_for_path(
|
|
189
|
+
file_path: str,
|
|
190
|
+
policy: GatePolicyConfig,
|
|
191
|
+
) -> LevelResolution:
|
|
192
|
+
"""Resolve effective complexity level for a file path.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
file_path: Relative file path (e.g., "src/core/engine.py").
|
|
196
|
+
policy: Loaded gate policy configuration.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
LevelResolution with level, skip_phases, skip_all, matched_rules.
|
|
200
|
+
|
|
201
|
+
Contract:
|
|
202
|
+
- Applies directory_overrides first (longest-prefix match).
|
|
203
|
+
- Then applies file_patterns (glob match).
|
|
204
|
+
- minimum_level upgrades, maximum_level downgrades.
|
|
205
|
+
- skip_phases merged (union).
|
|
206
|
+
- Deterministic: same inputs → same outputs (B10).
|
|
207
|
+
"""
|
|
208
|
+
level = policy.default_level
|
|
209
|
+
skip_phases: set[str] = set()
|
|
210
|
+
skip_all = False
|
|
211
|
+
matched_rules: list[str] = []
|
|
212
|
+
|
|
213
|
+
# Normalize path (remove leading ./ or /)
|
|
214
|
+
norm_path = file_path.lstrip("./")
|
|
215
|
+
|
|
216
|
+
# Step 1: Find longest-prefix matching directory override
|
|
217
|
+
best_dir_match: DirectoryOverride | None = None
|
|
218
|
+
best_dir_len = 0
|
|
219
|
+
for override in policy.directory_overrides:
|
|
220
|
+
opath = override.path.rstrip("/")
|
|
221
|
+
if norm_path.startswith(opath + "/") or norm_path == opath:
|
|
222
|
+
if len(opath) > best_dir_len:
|
|
223
|
+
best_dir_match = override
|
|
224
|
+
best_dir_len = len(opath)
|
|
225
|
+
|
|
226
|
+
if best_dir_match:
|
|
227
|
+
matched_rules.append(best_dir_match.path)
|
|
228
|
+
if best_dir_match.minimum_level:
|
|
229
|
+
if _level_ge(best_dir_match.minimum_level, level):
|
|
230
|
+
level = best_dir_match.minimum_level
|
|
231
|
+
if best_dir_match.maximum_level:
|
|
232
|
+
# Cap level at maximum_level (level must not exceed maximum)
|
|
233
|
+
if _LEVEL_ORDER.get(level, 0) > _LEVEL_ORDER.get(best_dir_match.maximum_level, 0):
|
|
234
|
+
level = best_dir_match.maximum_level
|
|
235
|
+
skip_phases.update(best_dir_match.skip_phases)
|
|
236
|
+
if best_dir_match.skip_all:
|
|
237
|
+
skip_all = True
|
|
238
|
+
|
|
239
|
+
# Step 2: Apply file pattern matches
|
|
240
|
+
for fp in policy.file_patterns:
|
|
241
|
+
# Match against both full path and basename
|
|
242
|
+
basename = norm_path.rsplit("/", 1)[-1] if "/" in norm_path else norm_path
|
|
243
|
+
if fnmatch(norm_path, fp.pattern) or fnmatch(basename, fp.pattern):
|
|
244
|
+
matched_rules.append(fp.pattern)
|
|
245
|
+
if fp.maximum_level:
|
|
246
|
+
# Cap level at maximum_level (level must not exceed maximum)
|
|
247
|
+
if _LEVEL_ORDER.get(level, 0) > _LEVEL_ORDER.get(fp.maximum_level, 0):
|
|
248
|
+
level = fp.maximum_level
|
|
249
|
+
skip_phases.update(fp.skip_phases)
|
|
250
|
+
if fp.skip_all:
|
|
251
|
+
skip_all = True
|
|
252
|
+
|
|
253
|
+
return LevelResolution(
|
|
254
|
+
level=level,
|
|
255
|
+
skip_phases=sorted(skip_phases),
|
|
256
|
+
skip_all=skip_all,
|
|
257
|
+
matched_rules=matched_rules,
|
|
258
|
+
)
|