forked from microsoft/apm
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodels.py
More file actions
261 lines (219 loc) · 9.8 KB
/
models.py
File metadata and controls
261 lines (219 loc) · 9.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
"""Data models for APM context."""
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, List, Union, Dict
@dataclass
class Chatmode:
"""Represents a chatmode primitive."""
name: str
file_path: Path
description: str
apply_to: Optional[str] # Glob pattern for file targeting (optional for chatmodes)
content: str
author: Optional[str] = None
version: Optional[str] = None
source: Optional[str] = None # Source of primitive: "local" or "dependency:{package_name}"
def validate(self) -> List[str]:
"""Validate chatmode structure.
Returns:
List[str]: List of validation errors.
"""
errors = []
if not self.description:
errors.append("Missing 'description' in frontmatter")
if not self.content.strip():
errors.append("Empty content")
return errors
@dataclass
class Instruction:
"""Represents an instruction primitive."""
name: str
file_path: Path
description: str
apply_to: str # Glob pattern for file targeting (required for instructions)
content: str
author: Optional[str] = None
version: Optional[str] = None
source: Optional[str] = None # Source of primitive: "local" or "dependency:{package_name}"
def validate(self) -> List[str]:
"""Validate instruction structure.
Returns:
List[str]: List of validation errors.
"""
errors = []
if not self.description:
errors.append("Missing 'description' in frontmatter")
if not self.apply_to:
errors.append("No 'applyTo' pattern specified -- instruction will apply globally")
if not self.content.strip():
errors.append("Empty content")
return errors
@dataclass
class Context:
"""Represents a context primitive."""
name: str
file_path: Path
content: str
description: Optional[str] = None
author: Optional[str] = None
version: Optional[str] = None
source: Optional[str] = None # Source of primitive: "local" or "dependency:{package_name}"
def validate(self) -> List[str]:
"""Validate context structure.
Returns:
List[str]: List of validation errors.
"""
errors = []
if not self.content.strip():
errors.append("Empty content")
return errors
@dataclass
class Skill:
"""Represents a SKILL.md primitive (package meta-guide).
SKILL.md is an optional file at the package root that describes
how to use the package. It's the fourth APM primitive type.
For Claude: SKILL.md is used natively for contextual activation.
For VSCode: SKILL.md is transformed to .agent.md for dropdown selection.
"""
name: str
file_path: Path
description: str
content: str
source: Optional[str] = None # Source of primitive: "local" or "dependency:{package_name}"
def validate(self) -> List[str]:
"""Validate skill structure.
Returns:
List[str]: List of validation errors.
"""
errors = []
if not self.name:
errors.append("Missing 'name' in frontmatter")
if not self.description:
errors.append("Missing 'description' in frontmatter")
if not self.content.strip():
errors.append("Empty content")
return errors
# Union type for all primitive types
Primitive = Union[Chatmode, Instruction, Context, Skill]
@dataclass
class PrimitiveConflict:
"""Represents a conflict between primitives from different sources."""
primitive_name: str
primitive_type: str # 'chatmode', 'instruction', 'context'
winning_source: str # Source that won the conflict
losing_sources: List[str] # Sources that lost the conflict
file_path: Path # Path of the winning primitive
def __str__(self) -> str:
"""String representation of the conflict."""
losing_list = ", ".join(self.losing_sources)
return f"{self.primitive_type} '{self.primitive_name}': {self.winning_source} overrides {losing_list}"
@dataclass
class PrimitiveCollection:
"""Collection of discovered primitives."""
chatmodes: List[Chatmode]
instructions: List[Instruction]
contexts: List[Context]
skills: List[Skill] # SKILL.md primitives (package meta-guides)
conflicts: List[PrimitiveConflict] # Track conflicts during discovery
def __init__(self):
self.chatmodes = []
self.instructions = []
self.contexts = []
self.skills = []
self.conflicts = []
# Name->index maps for O(1) conflict lookups (see #171)
self._chatmode_index: Dict[str, int] = {}
self._instruction_index: Dict[str, int] = {}
self._context_index: Dict[str, int] = {}
self._skill_index: Dict[str, int] = {}
def _index_for(self, primitive_type: str) -> Dict[str, int]:
"""Return the name->index map for the given primitive type."""
if primitive_type == "chatmode":
return self._chatmode_index
elif primitive_type == "instruction":
return self._instruction_index
elif primitive_type == "context":
return self._context_index
else:
return self._skill_index
def add_primitive(self, primitive: Primitive) -> None:
"""Add a primitive to the appropriate collection.
If a primitive with the same name already exists, the new primitive
will only be added if it has higher priority (lower priority primitives
are tracked as conflicts).
"""
if isinstance(primitive, Chatmode):
self._add_with_conflict_detection(primitive, self.chatmodes, "chatmode")
elif isinstance(primitive, Instruction):
self._add_with_conflict_detection(primitive, self.instructions, "instruction")
elif isinstance(primitive, Context):
self._add_with_conflict_detection(primitive, self.contexts, "context")
elif isinstance(primitive, Skill):
self._add_with_conflict_detection(primitive, self.skills, "skill")
else:
raise ValueError(f"Unknown primitive type: {type(primitive)}")
def _add_with_conflict_detection(self, new_primitive: Primitive, collection: List[Primitive], primitive_type: str) -> None:
"""Add primitive with conflict detection."""
name_index = self._index_for(primitive_type)
existing_index = name_index.get(new_primitive.name)
if existing_index is None:
# No conflict, just add the primitive
name_index[new_primitive.name] = len(collection)
collection.append(new_primitive)
else:
# Conflict detected - apply priority rules
existing = collection[existing_index]
# Priority rules:
# 1. Local always wins over dependency
# 2. Earlier dependency wins over later dependency
should_replace = self._should_replace_primitive(existing, new_primitive)
if should_replace:
# Replace existing with new primitive and record conflict
conflict = PrimitiveConflict(
primitive_name=new_primitive.name,
primitive_type=primitive_type,
winning_source=new_primitive.source or "unknown",
losing_sources=[existing.source or "unknown"],
file_path=new_primitive.file_path
)
self.conflicts.append(conflict)
collection[existing_index] = new_primitive
else:
# Keep existing and record that new primitive was ignored
conflict = PrimitiveConflict(
primitive_name=existing.name,
primitive_type=primitive_type,
winning_source=existing.source or "unknown",
losing_sources=[new_primitive.source or "unknown"],
file_path=existing.file_path
)
self.conflicts.append(conflict)
# Don't add new_primitive to collection
def _should_replace_primitive(self, existing: Primitive, new: Primitive) -> bool:
"""Determine if new primitive should replace existing based on priority."""
existing_source = existing.source or "unknown"
new_source = new.source or "unknown"
# Local always wins
if existing_source == "local":
return False # Never replace local
if new_source == "local":
return True # Always replace with local
# Both are dependencies - this shouldn't happen in correct usage
# since dependencies should be processed in order, but handle gracefully
return False # Keep first dependency (existing)
def all_primitives(self) -> List[Primitive]:
"""Get all primitives as a single list."""
return self.chatmodes + self.instructions + self.contexts + self.skills
def count(self) -> int:
"""Get total count of all primitives."""
return len(self.chatmodes) + len(self.instructions) + len(self.contexts) + len(self.skills)
def has_conflicts(self) -> bool:
"""Check if any conflicts were detected during discovery."""
return len(self.conflicts) > 0
def get_conflicts_by_type(self, primitive_type: str) -> List[PrimitiveConflict]:
"""Get conflicts for a specific primitive type."""
return [c for c in self.conflicts if c.primitive_type == primitive_type]
def get_primitives_by_source(self, source: str) -> List[Primitive]:
"""Get all primitives from a specific source."""
all_primitives = self.all_primitives()
return [p for p in all_primitives if p.source == source]