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,271 @@
1
+ """Pattern matching with variable extraction for rule file correspondence."""
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from fnmatch import fnmatch
6
+
7
+
8
+ class PatternError(Exception):
9
+ """Exception raised for invalid pattern syntax."""
10
+
11
+ pass
12
+
13
+
14
+ @dataclass
15
+ class MatchResult:
16
+ """Result of matching a file against a pattern."""
17
+
18
+ matched: bool
19
+ variables: dict[str, str] # Captured variable values
20
+
21
+ @classmethod
22
+ def no_match(cls) -> "MatchResult":
23
+ return cls(matched=False, variables={})
24
+
25
+ @classmethod
26
+ def match(cls, variables: dict[str, str] | None = None) -> "MatchResult":
27
+ return cls(matched=True, variables=variables or {})
28
+
29
+
30
+ def validate_pattern(pattern: str) -> None:
31
+ """
32
+ Validate pattern syntax.
33
+
34
+ Raises:
35
+ PatternError: If pattern has invalid syntax
36
+ """
37
+ # Check for unbalanced braces
38
+ brace_depth = 0
39
+ for i, char in enumerate(pattern):
40
+ if char == "{":
41
+ brace_depth += 1
42
+ elif char == "}":
43
+ brace_depth -= 1
44
+ if brace_depth < 0:
45
+ raise PatternError(f"Unmatched closing brace at position {i}")
46
+
47
+ if brace_depth > 0:
48
+ raise PatternError("Unclosed brace in pattern")
49
+
50
+ # Extract and validate variable names
51
+ var_pattern = r"\{([^}]*)\}"
52
+ seen_vars: set[str] = set()
53
+
54
+ for match in re.finditer(var_pattern, pattern):
55
+ var_name = match.group(1)
56
+
57
+ # Check for empty variable name
58
+ if not var_name:
59
+ raise PatternError("Empty variable name in pattern")
60
+
61
+ # Strip leading ** or * for validation
62
+ clean_name = var_name.lstrip("*")
63
+ if not clean_name:
64
+ # Just {*} or {**} is valid
65
+ continue
66
+
67
+ # Check for invalid characters in variable name
68
+ if "/" in clean_name or "\\" in clean_name:
69
+ raise PatternError(f"Invalid character in variable name: {var_name}")
70
+
71
+ # Check for duplicates (use clean name for comparison)
72
+ if clean_name in seen_vars:
73
+ raise PatternError(f"Duplicate variable: {clean_name}")
74
+ seen_vars.add(clean_name)
75
+
76
+
77
+ def pattern_to_regex(pattern: str) -> tuple[str, list[str]]:
78
+ """
79
+ Convert a pattern with {var} placeholders to a regex.
80
+
81
+ Variables:
82
+ - {path} or {**name} - Matches multiple path segments (.+)
83
+ - {name} or {*name} - Matches single path segment ([^/]+)
84
+
85
+ Args:
86
+ pattern: Pattern string like "src/{path}.py"
87
+
88
+ Returns:
89
+ Tuple of (regex_pattern, list_of_variable_names)
90
+
91
+ Raises:
92
+ PatternError: If pattern has invalid syntax
93
+ """
94
+ validate_pattern(pattern)
95
+
96
+ # Normalize path separators
97
+ pattern = pattern.replace("\\", "/")
98
+
99
+ result: list[str] = []
100
+ var_names: list[str] = []
101
+ pos = 0
102
+
103
+ # Parse pattern segments
104
+ while pos < len(pattern):
105
+ # Look for next variable
106
+ brace_start = pattern.find("{", pos)
107
+
108
+ if brace_start == -1:
109
+ # No more variables, escape the rest
110
+ result.append(re.escape(pattern[pos:]))
111
+ break
112
+
113
+ # Escape literal part before variable
114
+ if brace_start > pos:
115
+ result.append(re.escape(pattern[pos:brace_start]))
116
+
117
+ # Find end of variable
118
+ brace_end = pattern.find("}", brace_start)
119
+ if brace_end == -1:
120
+ raise PatternError("Unclosed brace in pattern")
121
+
122
+ var_spec = pattern[brace_start + 1 : brace_end]
123
+
124
+ # Determine variable type and name
125
+ if var_spec.startswith("**"):
126
+ # Explicit multi-segment: {**name}
127
+ var_name = var_spec[2:] or "path"
128
+ regex_part = f"(?P<{re.escape(var_name)}>.+)"
129
+ elif var_spec.startswith("*"):
130
+ # Explicit single-segment: {*name}
131
+ var_name = var_spec[1:] or "name"
132
+ regex_part = f"(?P<{re.escape(var_name)}>[^/]+)"
133
+ elif var_spec == "path":
134
+ # Conventional multi-segment
135
+ var_name = "path"
136
+ regex_part = "(?P<path>.+)"
137
+ else:
138
+ # Default single-segment (including custom names)
139
+ var_name = var_spec
140
+ regex_part = f"(?P<{re.escape(var_name)}>[^/]+)"
141
+
142
+ result.append(regex_part)
143
+ var_names.append(var_name)
144
+ pos = brace_end + 1
145
+
146
+ return "^" + "".join(result) + "$", var_names
147
+
148
+
149
+ def match_pattern(pattern: str, filepath: str) -> MatchResult:
150
+ """
151
+ Match a filepath against a pattern, extracting variables.
152
+
153
+ Args:
154
+ pattern: Pattern with {var} placeholders
155
+ filepath: File path to match
156
+
157
+ Returns:
158
+ MatchResult with matched=True and captured variables, or matched=False
159
+ """
160
+ # Normalize path separators
161
+ filepath = filepath.replace("\\", "/")
162
+
163
+ try:
164
+ regex, _ = pattern_to_regex(pattern)
165
+ except PatternError:
166
+ return MatchResult.no_match()
167
+
168
+ match = re.fullmatch(regex, filepath)
169
+ if match:
170
+ return MatchResult.match(match.groupdict())
171
+ return MatchResult.no_match()
172
+
173
+
174
+ def resolve_pattern(pattern: str, variables: dict[str, str]) -> str:
175
+ """
176
+ Substitute variables into a pattern to generate a filepath.
177
+
178
+ Args:
179
+ pattern: Pattern with {var} placeholders
180
+ variables: Dict of variable name -> value
181
+
182
+ Returns:
183
+ Resolved filepath string
184
+ """
185
+ result = pattern
186
+ for name, value in variables.items():
187
+ # Handle both {name} and {*name} / {**name} forms
188
+ result = result.replace(f"{{{name}}}", value)
189
+ result = result.replace(f"{{*{name}}}", value)
190
+ result = result.replace(f"{{**{name}}}", value)
191
+ return result
192
+
193
+
194
+ def matches_glob(file_path: str, pattern: str) -> bool:
195
+ """
196
+ Match a file path against a glob pattern, supporting ** for recursive matching.
197
+
198
+ This is for simple glob patterns without variable capture.
199
+
200
+ Args:
201
+ file_path: File path to check
202
+ pattern: Glob pattern (supports *, **, ?)
203
+
204
+ Returns:
205
+ True if matches
206
+ """
207
+ # Normalize path separators
208
+ file_path = file_path.replace("\\", "/")
209
+ pattern = pattern.replace("\\", "/")
210
+
211
+ # Handle ** patterns (recursive directory matching)
212
+ if "**" in pattern:
213
+ # Split pattern by **
214
+ parts = pattern.split("**")
215
+
216
+ if len(parts) == 2:
217
+ prefix, suffix = parts[0], parts[1]
218
+
219
+ # Remove leading/trailing slashes from suffix
220
+ suffix = suffix.lstrip("/")
221
+
222
+ # Check if prefix matches the start of the path
223
+ if prefix:
224
+ prefix = prefix.rstrip("/")
225
+ if not file_path.startswith(prefix + "/") and file_path != prefix:
226
+ return False
227
+ # Get the remaining path after prefix
228
+ remaining = file_path[len(prefix) :].lstrip("/")
229
+ else:
230
+ remaining = file_path
231
+
232
+ # If no suffix, any remaining path matches
233
+ if not suffix:
234
+ return True
235
+
236
+ # Check if suffix matches the end of any remaining path segment
237
+ remaining_parts = remaining.split("/")
238
+ for i in range(len(remaining_parts)):
239
+ test_path = "/".join(remaining_parts[i:])
240
+ if fnmatch(test_path, suffix):
241
+ return True
242
+ # Also try just the filename
243
+ if fnmatch(remaining_parts[-1], suffix):
244
+ return True
245
+
246
+ return False
247
+
248
+ # Simple pattern without **
249
+ return fnmatch(file_path, pattern)
250
+
251
+
252
+ def matches_any_pattern(file_path: str, patterns: list[str]) -> bool:
253
+ """
254
+ Check if a file path matches any of the given glob patterns.
255
+
256
+ Args:
257
+ file_path: File path to check (relative path)
258
+ patterns: List of glob patterns to match against
259
+
260
+ Returns:
261
+ True if the file matches any pattern
262
+ """
263
+ for pattern in patterns:
264
+ if matches_glob(file_path, pattern):
265
+ return True
266
+ return False
267
+
268
+
269
+ def has_variables(pattern: str) -> bool:
270
+ """Check if a pattern contains variable placeholders."""
271
+ return "{" in pattern and "}" in pattern