deepwork 0.2.0__py3-none-any.whl → 0.3.1__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.
Files changed (54) hide show
  1. deepwork/cli/install.py +116 -71
  2. deepwork/cli/sync.py +20 -20
  3. deepwork/core/adapters.py +88 -51
  4. deepwork/core/command_executor.py +173 -0
  5. deepwork/core/generator.py +148 -31
  6. deepwork/core/hooks_syncer.py +51 -25
  7. deepwork/core/parser.py +8 -0
  8. deepwork/core/pattern_matcher.py +271 -0
  9. deepwork/core/rules_parser.py +559 -0
  10. deepwork/core/rules_queue.py +321 -0
  11. deepwork/hooks/README.md +181 -0
  12. deepwork/hooks/__init__.py +77 -1
  13. deepwork/hooks/claude_hook.sh +55 -0
  14. deepwork/hooks/gemini_hook.sh +55 -0
  15. deepwork/hooks/rules_check.py +700 -0
  16. deepwork/hooks/wrapper.py +363 -0
  17. deepwork/schemas/job_schema.py +14 -1
  18. deepwork/schemas/rules_schema.py +135 -0
  19. deepwork/standard_jobs/deepwork_jobs/job.yml +35 -53
  20. deepwork/standard_jobs/deepwork_jobs/steps/define.md +9 -6
  21. deepwork/standard_jobs/deepwork_jobs/steps/implement.md +28 -26
  22. deepwork/standard_jobs/deepwork_jobs/steps/learn.md +2 -2
  23. deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh +30 -0
  24. deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml +8 -0
  25. deepwork/standard_jobs/deepwork_rules/job.yml +47 -0
  26. deepwork/standard_jobs/deepwork_rules/rules/.gitkeep +13 -0
  27. deepwork/standard_jobs/deepwork_rules/rules/api-documentation-sync.md.example +10 -0
  28. deepwork/standard_jobs/deepwork_rules/rules/readme-documentation.md.example +10 -0
  29. deepwork/standard_jobs/deepwork_rules/rules/security-review.md.example +11 -0
  30. deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md +46 -0
  31. deepwork/standard_jobs/deepwork_rules/rules/source-test-pairing.md.example +13 -0
  32. deepwork/standard_jobs/deepwork_rules/steps/define.md +249 -0
  33. deepwork/templates/claude/skill-job-meta.md.jinja +70 -0
  34. deepwork/templates/claude/skill-job-step.md.jinja +198 -0
  35. deepwork/templates/gemini/skill-job-meta.toml.jinja +76 -0
  36. deepwork/templates/gemini/skill-job-step.toml.jinja +147 -0
  37. {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/METADATA +56 -25
  38. deepwork-0.3.1.dist-info/RECORD +62 -0
  39. deepwork/core/policy_parser.py +0 -295
  40. deepwork/hooks/evaluate_policies.py +0 -376
  41. deepwork/schemas/policy_schema.py +0 -78
  42. deepwork/standard_jobs/deepwork_policy/hooks/capture_prompt_work_tree.sh +0 -27
  43. deepwork/standard_jobs/deepwork_policy/hooks/global_hooks.yml +0 -8
  44. deepwork/standard_jobs/deepwork_policy/hooks/policy_stop_hook.sh +0 -56
  45. deepwork/standard_jobs/deepwork_policy/job.yml +0 -35
  46. deepwork/standard_jobs/deepwork_policy/steps/define.md +0 -195
  47. deepwork/templates/claude/command-job-step.md.jinja +0 -210
  48. deepwork/templates/default_policy.yml +0 -53
  49. deepwork/templates/gemini/command-job-step.toml.jinja +0 -169
  50. deepwork-0.2.0.dist-info/RECORD +0 -49
  51. /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/user_prompt_submit.sh +0 -0
  52. {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/WHEEL +0 -0
  53. {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/entry_points.txt +0 -0
  54. {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,62 @@
1
+ deepwork/__init__.py,sha256=vcMnJioxhfoL6kGh4FM51Vk9UoLnQ76g6Ms7XDUItYA,748
2
+ deepwork/cli/__init__.py,sha256=3SqmfcP2xqutiCYAbajFDJTjr2pOLydqTN0NN-FTsIE,33
3
+ deepwork/cli/install.py,sha256=USSmALGWpCBc4kinAxTbt8j2AcSJ4vVNtYpG5s0M0sE,12897
4
+ deepwork/cli/main.py,sha256=m6tVnfSy03azI9Ky6ySEMat_UdLEMzVT3SsRKQWigvA,471
5
+ deepwork/cli/sync.py,sha256=d608WDOzOVpYbS6RF-xoonK3qj22WyewRFJrva_qXaQ,6251
6
+ deepwork/core/__init__.py,sha256=1g869QuwsYzNjQONneng2OMc6HKt-tlBCaxJbMMfoho,36
7
+ deepwork/core/adapters.py,sha256=OeFw4Db6nEKnSDLgol-mhYTx-GV9XEBp_4qI1WuX7KA,14410
8
+ deepwork/core/command_executor.py,sha256=l2XZ9EpCPY6dWqCPGUjq2jczq154evp2IvPBO4O6Xpg,4733
9
+ deepwork/core/detector.py,sha256=PThpFLH-ZVL8UqjVdGSnGA0mFbhc6_rp6V3_Yzw97kI,2466
10
+ deepwork/core/generator.py,sha256=0tauPRUA_D9o6BEksx9BJFV9lOlpgK1Te8fI2gxP0kc,14186
11
+ deepwork/core/hooks_syncer.py,sha256=kEr8bLgWJXPb1C8AOv2fxUnIX1j9zgfPahyYIZUvfko,6771
12
+ deepwork/core/parser.py,sha256=R799f_IC9zhQNROnIC534LRZ_TMYp5gaqwH3nGSrI9Y,9936
13
+ deepwork/core/pattern_matcher.py,sha256=wtrxFNSCMxk-47Nw4Ca-8769ll4weRvvQBLGsRERcKM,8073
14
+ deepwork/core/rules_parser.py,sha256=r_Pay0Ug9FDGCEccpLnQjsQjSEUmECceh0Ac3Ayw4oE,17774
15
+ deepwork/core/rules_queue.py,sha256=uCNuQzohUyEYGnCUv1nMDEYoLeTuiSHN44-kuXbSLDU,10069
16
+ deepwork/hooks/README.md,sha256=t4rAd68oRx0-cbln4kfV0Sk2IWhGAGR7HB4McgBXTYE,4391
17
+ deepwork/hooks/__init__.py,sha256=aUWXT5b8MaKzYp7x2Bwezj_NQwAJ-6a8nXxDz0o0qsY,1882
18
+ deepwork/hooks/claude_hook.sh,sha256=VjbRVOUirIhFU7CcsafWk25HTIUzZmCrWqNPiY9e17o,1539
19
+ deepwork/hooks/gemini_hook.sh,sha256=wVfnZs7VvUNFgOkZnhUpbMYlVyy7lQWG_u8PqDQbL8g,1534
20
+ deepwork/hooks/rules_check.py,sha256=dmXnxOkpT1KH32uQkYeb_Btwykykb4-Gr4H4C86IIfo,23562
21
+ deepwork/hooks/wrapper.py,sha256=mSps8tWQEdBThXwDViOtNad0d9pFaNPF8MFpG-Rhuds,11517
22
+ deepwork/schemas/__init__.py,sha256=PpydKb_oaTv8lYapN_nV-Tl_OUCoSM_okvsEJ8gNTpI,41
23
+ deepwork/schemas/job_schema.py,sha256=R_82YmLWGn6lV1arjJ7_EzYFMYoTy1tzdUBk34mdNSY,9388
24
+ deepwork/schemas/rules_schema.py,sha256=CNv0_ambXTHZKFy81VCpew6KmHslSQPwLG7yd2x-z_0,4666
25
+ deepwork/standard_jobs/deepwork_jobs/AGENTS.md,sha256=Y6I4jZ8DfN0RFY3UF5bgQRZvL7wQD9P0lgE7EZM6CGI,2252
26
+ deepwork/standard_jobs/deepwork_jobs/job.yml,sha256=fNZVh_iW0sUXUCs_2YcIi2CVVLcnrM95YE9Zq4cZeNI,5399
27
+ deepwork/standard_jobs/deepwork_jobs/make_new_job.sh,sha256=JArfFU9lEaJPRsXRL3rU1IMt2p6Bq0s2C9f98aJ7Mxg,3878
28
+ deepwork/standard_jobs/deepwork_jobs/steps/define.md,sha256=j7S2ONQ-hih4ksIwge91yOD1SGa5VZcXw-J_RVhbFqg,13179
29
+ deepwork/standard_jobs/deepwork_jobs/steps/implement.md,sha256=Kgz6xBSFWNjKcTzRYOOyHox1cBl6SSmPbBaVPnKfbAY,10603
30
+ deepwork/standard_jobs/deepwork_jobs/steps/learn.md,sha256=YeWoMgEvpsjDyknMGjopRa4dnfRbyxCar3f4AMXi6K0,10179
31
+ deepwork/standard_jobs/deepwork_jobs/steps/supplemental_file_references.md,sha256=uKDEwB3TxMLK-Zim3QQfkvaW5W6AVWHjWnH25aY6wCw,1478
32
+ deepwork/standard_jobs/deepwork_jobs/templates/agents.md.template,sha256=SUJL862C6-DnT9lK3sNIZ5T2wVgXN4YRph4FrKtFnLo,739
33
+ deepwork/standard_jobs/deepwork_jobs/templates/job.yml.example,sha256=roRi6sIGFGmPCkoVW26HfuTBjAO8-pPsxI5-Gfg3rc0,2361
34
+ deepwork/standard_jobs/deepwork_jobs/templates/job.yml.template,sha256=3I-VQvqXVJNZ_Vb5Ik28JsrEbGKbyLTZcuKxdEmV5s0,1789
35
+ deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.example,sha256=HXcjVaQz2HsDiA4ClnIeLvysVOGrFJ_5Tr-pm6dhdwc,2706
36
+ deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.template,sha256=6n9jFFuda4r549Oo-LBPKixFD3NvDl5MwEg5V7ItQBg,1286
37
+ deepwork/standard_jobs/deepwork_rules/job.yml,sha256=N61yhHmDeN6gldcQ7FytENmNe-r0G-C72ziEVizz7Jo,2164
38
+ deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh,sha256=e1iPltUxA6giTLD5TcyjWAr_3JCb7wLwYIqE8WHqCYs,1287
39
+ deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml,sha256=uZkGMROaICKik6-Mfa1dM-8608Y9L-VrYG5YAbVN8yw,186
40
+ deepwork/standard_jobs/deepwork_rules/hooks/user_prompt_submit.sh,sha256=TxwYb7kBW-cfHmcQoruJBjCTWvdWbQVQIMplNgzMuOs,498
41
+ deepwork/standard_jobs/deepwork_rules/rules/.gitkeep,sha256=tOhdLTAbVXm_Z4Gl85recPNonTC0kFwzw6NKpeqTmH8,337
42
+ deepwork/standard_jobs/deepwork_rules/rules/api-documentation-sync.md.example,sha256=YaTuv-LseeqYSG6mk2LzORfyGJ0bGaXuTB8IJkwzrDI,260
43
+ deepwork/standard_jobs/deepwork_rules/rules/readme-documentation.md.example,sha256=2uZHS-LjZfL81i7COJvIA0Q5lqa-aWyLx060ouSnlrw,305
44
+ deepwork/standard_jobs/deepwork_rules/rules/security-review.md.example,sha256=gPAk5bWLqXm5mO6vlYAU7FGDQbNlXE-Fuf2mU4sru80,285
45
+ deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md,sha256=H-o8X92AerumY2nimLzX-PniN2FGWAPI4tNGMpEo7nM,1457
46
+ deepwork/standard_jobs/deepwork_rules/rules/source-test-pairing.md.example,sha256=Uwg0l85nB69yY8MWwF46EUwx3UJ3qIHXb2ywxp_VNW0,320
47
+ deepwork/standard_jobs/deepwork_rules/steps/define.md,sha256=TP3j90bh0zsGQniAo2GkX9xbLtB_XkaA4IudcbAZ0YY,8349
48
+ deepwork/templates/__init__.py,sha256=APvjx_u7eRUerw9yA_fJ1ZqCzYA-FWUCV9HCz0RgjOc,50
49
+ deepwork/templates/claude/skill-job-meta.md.jinja,sha256=109cnDYFpfIJhW4VBu0lJulvmBubIhoUUZyYvpqeJdM,2021
50
+ deepwork/templates/claude/skill-job-step.md.jinja,sha256=ENsfW1PTBR5LwVbox3BMqnaUZRDNuvtOETxnpB8luCE,5957
51
+ deepwork/templates/gemini/skill-job-meta.toml.jinja,sha256=FOkRNP3pGbvwcwwt3k6WF_qsCIkj1AbLYtF3mlkqAsA,2157
52
+ deepwork/templates/gemini/skill-job-step.toml.jinja,sha256=RghMd_Yj0y2xmVdG5WOuk6bDV3UPVYaWS8W6qLYSalw,4262
53
+ deepwork/utils/__init__.py,sha256=AtvE49IFI8Rg36O4cNIlzB-oxvkW3apFgXExn8GSk6s,38
54
+ deepwork/utils/fs.py,sha256=94OUvUrqGebjHVtnjd5vXL6DalKNdpRu-iAPsHvAPjI,3499
55
+ deepwork/utils/git.py,sha256=J4tAB1zE6-WMAEHbarevhmSvvPLkeKBpiRv1UxUVwYk,3748
56
+ deepwork/utils/validation.py,sha256=SyFg9fIu1JCDMbssQgJRCTUNToDNcINccn8lje-tjts,851
57
+ deepwork/utils/yaml_utils.py,sha256=X8c9yEqxEgw5CdPQ23f1Wz8SSP783MMGKyGV_2SKaNU,2454
58
+ deepwork-0.3.1.dist-info/METADATA,sha256=kbBslaPiI2kmRcWmq0Pjty9Z2gyzU6hvoOqGWHrKj6k,12277
59
+ deepwork-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
60
+ deepwork-0.3.1.dist-info/entry_points.txt,sha256=RhJBySzm619kh-yIdsAyfFXInAlY8Jm-39FLIBcOj2s,51
61
+ deepwork-0.3.1.dist-info/licenses/LICENSE.md,sha256=W0EtJVYf0cQ_awukOCW1ETwNSpV2RKqnAGfoOjyz_K8,4126
62
+ deepwork-0.3.1.dist-info/RECORD,,
@@ -1,295 +0,0 @@
1
- """Policy definition parser."""
2
-
3
- from dataclasses import dataclass, field
4
- from fnmatch import fnmatch
5
- from pathlib import Path
6
- from typing import Any
7
-
8
- import yaml
9
-
10
- from deepwork.schemas.policy_schema import POLICY_SCHEMA
11
- from deepwork.utils.validation import ValidationError, validate_against_schema
12
-
13
-
14
- class PolicyParseError(Exception):
15
- """Exception raised for policy parsing errors."""
16
-
17
- pass
18
-
19
-
20
- # Valid compare_to values
21
- COMPARE_TO_VALUES = frozenset({"base", "default_tip", "prompt"})
22
- DEFAULT_COMPARE_TO = "base"
23
-
24
-
25
- @dataclass
26
- class Policy:
27
- """Represents a single policy definition."""
28
-
29
- name: str
30
- triggers: list[str] # Normalized to list
31
- safety: list[str] = field(default_factory=list) # Normalized to list, empty if not specified
32
- instructions: str = "" # Resolved content (either inline or from file)
33
- compare_to: str = DEFAULT_COMPARE_TO # What to compare against: base, default_tip, or prompt
34
-
35
- @classmethod
36
- def from_dict(cls, data: dict[str, Any], base_dir: Path | None = None) -> "Policy":
37
- """
38
- Create Policy from dictionary.
39
-
40
- Args:
41
- data: Parsed YAML data for a single policy
42
- base_dir: Base directory for resolving instructions_file paths
43
-
44
- Returns:
45
- Policy instance
46
-
47
- Raises:
48
- PolicyParseError: If instructions cannot be resolved
49
- """
50
- # Normalize trigger to list
51
- trigger = data["trigger"]
52
- triggers = [trigger] if isinstance(trigger, str) else list(trigger)
53
-
54
- # Normalize safety to list (empty if not present)
55
- safety_data = data.get("safety", [])
56
- safety = [safety_data] if isinstance(safety_data, str) else list(safety_data)
57
-
58
- # Resolve instructions
59
- if "instructions" in data:
60
- instructions = data["instructions"]
61
- elif "instructions_file" in data:
62
- if base_dir is None:
63
- raise PolicyParseError(
64
- f"Policy '{data['name']}' uses instructions_file but no base_dir provided"
65
- )
66
- instructions_path = base_dir / data["instructions_file"]
67
- if not instructions_path.exists():
68
- raise PolicyParseError(
69
- f"Policy '{data['name']}' instructions file not found: {instructions_path}"
70
- )
71
- try:
72
- instructions = instructions_path.read_text()
73
- except Exception as e:
74
- raise PolicyParseError(
75
- f"Policy '{data['name']}' failed to read instructions file: {e}"
76
- ) from e
77
- else:
78
- # Schema should catch this, but be defensive
79
- raise PolicyParseError(
80
- f"Policy '{data['name']}' must have either 'instructions' or 'instructions_file'"
81
- )
82
-
83
- # Get compare_to (defaults to DEFAULT_COMPARE_TO)
84
- compare_to = data.get("compare_to", DEFAULT_COMPARE_TO)
85
-
86
- return cls(
87
- name=data["name"],
88
- triggers=triggers,
89
- safety=safety,
90
- instructions=instructions,
91
- compare_to=compare_to,
92
- )
93
-
94
-
95
- def matches_pattern(file_path: str, patterns: list[str]) -> bool:
96
- """
97
- Check if a file path matches any of the given glob patterns.
98
-
99
- Args:
100
- file_path: File path to check (relative path)
101
- patterns: List of glob patterns to match against
102
-
103
- Returns:
104
- True if the file matches any pattern
105
- """
106
- for pattern in patterns:
107
- if _matches_glob(file_path, pattern):
108
- return True
109
- return False
110
-
111
-
112
- def _matches_glob(file_path: str, pattern: str) -> bool:
113
- """
114
- Match a file path against a glob pattern, supporting ** for recursive matching.
115
-
116
- Args:
117
- file_path: File path to check
118
- pattern: Glob pattern (supports *, **, ?)
119
-
120
- Returns:
121
- True if matches
122
- """
123
- # Normalize path separators
124
- file_path = file_path.replace("\\", "/")
125
- pattern = pattern.replace("\\", "/")
126
-
127
- # Handle ** patterns (recursive directory matching)
128
- if "**" in pattern:
129
- # Split pattern by **
130
- parts = pattern.split("**")
131
-
132
- if len(parts) == 2:
133
- prefix, suffix = parts[0], parts[1]
134
-
135
- # Remove leading/trailing slashes from suffix
136
- suffix = suffix.lstrip("/")
137
-
138
- # Check if prefix matches the start of the path
139
- if prefix:
140
- prefix = prefix.rstrip("/")
141
- if not file_path.startswith(prefix + "/") and file_path != prefix:
142
- return False
143
- # Get the remaining path after prefix
144
- remaining = file_path[len(prefix) :].lstrip("/")
145
- else:
146
- remaining = file_path
147
-
148
- # If no suffix, any remaining path matches
149
- if not suffix:
150
- return True
151
-
152
- # Check if suffix matches the end of any remaining path segment
153
- # For pattern "src/**/*.py", suffix is "*.py"
154
- # We need to match *.py against the filename portion
155
- remaining_parts = remaining.split("/")
156
- for i in range(len(remaining_parts)):
157
- test_path = "/".join(remaining_parts[i:])
158
- if fnmatch(test_path, suffix):
159
- return True
160
- # Also try just the filename
161
- if fnmatch(remaining_parts[-1], suffix):
162
- return True
163
-
164
- return False
165
-
166
- # Simple pattern without **
167
- return fnmatch(file_path, pattern)
168
-
169
-
170
- def evaluate_policy(policy: Policy, changed_files: list[str]) -> bool:
171
- """
172
- Evaluate whether a policy should fire based on changed files.
173
-
174
- A policy fires if:
175
- - At least one changed file matches a trigger pattern
176
- - AND no changed file matches a safety pattern
177
-
178
- Args:
179
- policy: Policy to evaluate
180
- changed_files: List of changed file paths (relative)
181
-
182
- Returns:
183
- True if the policy should fire
184
- """
185
- # Check if any trigger matches
186
- trigger_matched = False
187
- for file_path in changed_files:
188
- if matches_pattern(file_path, policy.triggers):
189
- trigger_matched = True
190
- break
191
-
192
- if not trigger_matched:
193
- return False
194
-
195
- # Check if any safety pattern matches
196
- if policy.safety:
197
- for file_path in changed_files:
198
- if matches_pattern(file_path, policy.safety):
199
- # Safety file was also changed, don't fire
200
- return False
201
-
202
- return True
203
-
204
-
205
- def evaluate_policies(
206
- policies: list[Policy],
207
- changed_files: list[str],
208
- promised_policies: set[str] | None = None,
209
- ) -> list[Policy]:
210
- """
211
- Evaluate which policies should fire.
212
-
213
- Args:
214
- policies: List of policies to evaluate
215
- changed_files: List of changed file paths (relative)
216
- promised_policies: Set of policy names that have been marked as addressed
217
- via <promise> tags (these are skipped)
218
-
219
- Returns:
220
- List of policies that should fire (trigger matches, no safety match, not promised)
221
- """
222
- if promised_policies is None:
223
- promised_policies = set()
224
-
225
- fired_policies = []
226
- for policy in policies:
227
- # Skip if already promised/addressed
228
- if policy.name in promised_policies:
229
- continue
230
-
231
- if evaluate_policy(policy, changed_files):
232
- fired_policies.append(policy)
233
-
234
- return fired_policies
235
-
236
-
237
- def parse_policy_file(policy_path: Path | str, base_dir: Path | None = None) -> list[Policy]:
238
- """
239
- Parse policy definitions from a YAML file.
240
-
241
- Args:
242
- policy_path: Path to .deepwork.policy.yml file
243
- base_dir: Base directory for resolving instructions_file paths.
244
- Defaults to the directory containing the policy file.
245
-
246
- Returns:
247
- List of parsed Policy objects
248
-
249
- Raises:
250
- PolicyParseError: If parsing fails or validation errors occur
251
- """
252
- policy_path = Path(policy_path)
253
-
254
- if not policy_path.exists():
255
- raise PolicyParseError(f"Policy file does not exist: {policy_path}")
256
-
257
- if not policy_path.is_file():
258
- raise PolicyParseError(f"Policy path is not a file: {policy_path}")
259
-
260
- # Default base_dir to policy file's directory
261
- if base_dir is None:
262
- base_dir = policy_path.parent
263
-
264
- # Load YAML (policies are stored as a list, not a dict)
265
- try:
266
- with open(policy_path, encoding="utf-8") as f:
267
- policy_data = yaml.safe_load(f)
268
- except yaml.YAMLError as e:
269
- raise PolicyParseError(f"Failed to parse policy YAML: {e}") from e
270
- except OSError as e:
271
- raise PolicyParseError(f"Failed to read policy file: {e}") from e
272
-
273
- # Handle empty file or null content
274
- if policy_data is None:
275
- return []
276
-
277
- # Validate it's a list (schema expects array)
278
- if not isinstance(policy_data, list):
279
- raise PolicyParseError(
280
- f"Policy file must contain a list of policies, got {type(policy_data).__name__}"
281
- )
282
-
283
- # Validate against schema
284
- try:
285
- validate_against_schema(policy_data, POLICY_SCHEMA)
286
- except ValidationError as e:
287
- raise PolicyParseError(f"Policy definition validation failed: {e}") from e
288
-
289
- # Parse into dataclasses
290
- policies = []
291
- for policy_item in policy_data:
292
- policy = Policy.from_dict(policy_item, base_dir)
293
- policies.append(policy)
294
-
295
- return policies