xrefkit 0.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.
- xrefkit/__init__.py +5 -0
- xrefkit/__main__.py +5 -0
- xrefkit/catalog_cli.py +57 -0
- xrefkit/cli.py +71 -0
- xrefkit/contracts.py +297 -0
- xrefkit/ctx.py +160 -0
- xrefkit/dashboard.py +1220 -0
- xrefkit/discovery.py +85 -0
- xrefkit/gate.py +428 -0
- xrefkit/goalstate.py +555 -0
- xrefkit/hashing.py +18 -0
- xrefkit/import_skill.py +469 -0
- xrefkit/instance.py +133 -0
- xrefkit/loaders.py +77 -0
- xrefkit/mcp/__init__.py +26 -0
- xrefkit/mcp/audit.py +168 -0
- xrefkit/mcp/bootstrap.py +337 -0
- xrefkit/mcp/catalog.py +2638 -0
- xrefkit/mcp/cli.py +173 -0
- xrefkit/mcp/client_cache.py +356 -0
- xrefkit/mcp/context_registry.py +277 -0
- xrefkit/mcp/contracts.py +243 -0
- xrefkit/mcp/dist.py +234 -0
- xrefkit/mcp/ownership.py +246 -0
- xrefkit/mcp/repository.py +217 -0
- xrefkit/mcp/schemas.py +349 -0
- xrefkit/mcp/server.py +773 -0
- xrefkit/mcp/startup_contract_pack.py +154 -0
- xrefkit/mcp_tools.py +47 -0
- xrefkit/models/__init__.py +41 -0
- xrefkit/models/common.py +185 -0
- xrefkit/models/effective_bundle.py +131 -0
- xrefkit/models/local_manifest.py +217 -0
- xrefkit/models/package_manifest.py +126 -0
- xrefkit/models/run_log.py +276 -0
- xrefkit/models/server_config.py +160 -0
- xrefkit/models/skill_definition.py +131 -0
- xrefkit/operations_cli.py +670 -0
- xrefkit/ownership.py +276 -0
- xrefkit/packmeta.py +289 -0
- xrefkit/registry.py +334 -0
- xrefkit/resolver.py +252 -0
- xrefkit/resource_provider.py +187 -0
- xrefkit/resources/base/contracts.json +178 -0
- xrefkit/resources/base/current.json +6 -0
- xrefkit/resources/base/generations/7a682a5272907354/contracts.json +178 -0
- xrefkit/resources/base/generations/7a682a5272907354/model_body.md +22 -0
- xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +178 -0
- xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +22 -0
- xrefkit/resources/base/model_body.md +22 -0
- xrefkit/runlog.py +45 -0
- xrefkit/skillmeta.py +1034 -0
- xrefkit/skillrun.py +2381 -0
- xrefkit/structure_catalog.py +199 -0
- xrefkit/tools/__init__.py +119 -0
- xrefkit/tools/__main__.py +4 -0
- xrefkit/v2_cli.py +130 -0
- xrefkit/workspace.py +117 -0
- xrefkit/xref.py +1048 -0
- xrefkit-0.3.0.dist-info/METADATA +203 -0
- xrefkit-0.3.0.dist-info/RECORD +65 -0
- xrefkit-0.3.0.dist-info/WHEEL +5 -0
- xrefkit-0.3.0.dist-info/entry_points.txt +2 -0
- xrefkit-0.3.0.dist-info/licenses/LICENSE +21 -0
- xrefkit-0.3.0.dist-info/top_level.txt +1 -0
xrefkit/registry.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""In-memory registries for XRefKit v2 MVP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .models import LocalDomainSkill, LocalManifest, PackageManifest, SkillDefinition
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _contained_path(root: Path, relative_path: str, *, label: str) -> Path:
|
|
12
|
+
root = root.resolve()
|
|
13
|
+
candidate = (root / relative_path).resolve()
|
|
14
|
+
try:
|
|
15
|
+
candidate.relative_to(root)
|
|
16
|
+
except ValueError as exc:
|
|
17
|
+
raise ValueError(f"{label} escapes root: {relative_path}") from exc
|
|
18
|
+
return candidate
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class XidAssetRecord:
|
|
23
|
+
xid: str
|
|
24
|
+
path: Path
|
|
25
|
+
root: Path
|
|
26
|
+
source_type: str
|
|
27
|
+
asset_type: str
|
|
28
|
+
includeable: bool = False
|
|
29
|
+
package_id: str | None = None
|
|
30
|
+
local_id: str | None = None
|
|
31
|
+
fragment_id: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class PackageRecord:
|
|
36
|
+
manifest: PackageManifest
|
|
37
|
+
root: Path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class SkillRecord:
|
|
42
|
+
package_id: str | None
|
|
43
|
+
skill_id: str
|
|
44
|
+
xid: str
|
|
45
|
+
definition: SkillDefinition | LocalDomainSkill
|
|
46
|
+
path: Path
|
|
47
|
+
root: Path
|
|
48
|
+
source_type: str
|
|
49
|
+
local_id: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class KnowledgeRecord:
|
|
54
|
+
id: str
|
|
55
|
+
xid: str
|
|
56
|
+
path: Path
|
|
57
|
+
source_type: str
|
|
58
|
+
package_id: str | None = None
|
|
59
|
+
local_id: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class XidRegistry:
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
self._items: dict[str, object] = {}
|
|
65
|
+
|
|
66
|
+
def add(self, xid: str, record: object) -> None:
|
|
67
|
+
if xid in self._items:
|
|
68
|
+
raise ValueError(f"duplicate XID: {xid}")
|
|
69
|
+
self._items[xid] = record
|
|
70
|
+
|
|
71
|
+
def add_if_absent(self, xid: str, record: object) -> None:
|
|
72
|
+
if xid not in self._items:
|
|
73
|
+
self._items[xid] = record
|
|
74
|
+
|
|
75
|
+
def get(self, xid: str) -> object | None:
|
|
76
|
+
return self._items.get(xid)
|
|
77
|
+
|
|
78
|
+
def require(self, xid: str) -> object:
|
|
79
|
+
record = self.get(xid)
|
|
80
|
+
if record is None:
|
|
81
|
+
raise KeyError(f"unknown XID: {xid}")
|
|
82
|
+
return record
|
|
83
|
+
|
|
84
|
+
def require_asset(self, xid: str) -> XidAssetRecord:
|
|
85
|
+
record = self.require(xid)
|
|
86
|
+
if not isinstance(record, XidAssetRecord):
|
|
87
|
+
raise TypeError(f"XID is not a file-backed asset: {xid}")
|
|
88
|
+
return record
|
|
89
|
+
|
|
90
|
+
def __contains__(self, xid: str) -> bool:
|
|
91
|
+
return xid in self._items
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PackageRegistry:
|
|
95
|
+
def __init__(self) -> None:
|
|
96
|
+
self._packages: dict[str, PackageRecord] = {}
|
|
97
|
+
|
|
98
|
+
def add(self, manifest: PackageManifest, root: str | Path) -> PackageRecord:
|
|
99
|
+
if manifest.package_id in self._packages:
|
|
100
|
+
raise ValueError(f"duplicate package_id: {manifest.package_id}")
|
|
101
|
+
record = PackageRecord(manifest=manifest, root=Path(root))
|
|
102
|
+
self._packages[manifest.package_id] = record
|
|
103
|
+
return record
|
|
104
|
+
|
|
105
|
+
def get(self, package_id: str) -> PackageRecord | None:
|
|
106
|
+
return self._packages.get(package_id)
|
|
107
|
+
|
|
108
|
+
def require(self, package_id: str) -> PackageRecord:
|
|
109
|
+
record = self.get(package_id)
|
|
110
|
+
if record is None:
|
|
111
|
+
raise KeyError(f"unknown package_id: {package_id}")
|
|
112
|
+
return record
|
|
113
|
+
|
|
114
|
+
def list(self) -> list[PackageRecord]:
|
|
115
|
+
return list(self._packages.values())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class SkillRegistry:
|
|
119
|
+
def __init__(self) -> None:
|
|
120
|
+
self._skills: dict[str, SkillRecord] = {}
|
|
121
|
+
|
|
122
|
+
def add(self, record: SkillRecord) -> None:
|
|
123
|
+
if record.skill_id in self._skills:
|
|
124
|
+
raise ValueError(f"duplicate skill_id: {record.skill_id}")
|
|
125
|
+
self._skills[record.skill_id] = record
|
|
126
|
+
|
|
127
|
+
def get(self, skill_id: str) -> SkillRecord | None:
|
|
128
|
+
return self._skills.get(skill_id)
|
|
129
|
+
|
|
130
|
+
def require(self, skill_id: str) -> SkillRecord:
|
|
131
|
+
record = self.get(skill_id)
|
|
132
|
+
if record is None:
|
|
133
|
+
raise KeyError(f"unknown skill_id: {skill_id}")
|
|
134
|
+
return record
|
|
135
|
+
|
|
136
|
+
def list(self) -> list[SkillRecord]:
|
|
137
|
+
return list(self._skills.values())
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class KnowledgeRegistry:
|
|
141
|
+
def __init__(self) -> None:
|
|
142
|
+
self._knowledge: dict[str, KnowledgeRecord] = {}
|
|
143
|
+
|
|
144
|
+
def add(self, record: KnowledgeRecord) -> None:
|
|
145
|
+
if record.id in self._knowledge:
|
|
146
|
+
raise ValueError(f"duplicate knowledge id: {record.id}")
|
|
147
|
+
self._knowledge[record.id] = record
|
|
148
|
+
|
|
149
|
+
def list(self) -> list[KnowledgeRecord]:
|
|
150
|
+
return list(self._knowledge.values())
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class XRefKitRegistry:
|
|
154
|
+
def __init__(self) -> None:
|
|
155
|
+
self.packages = PackageRegistry()
|
|
156
|
+
self.skills = SkillRegistry()
|
|
157
|
+
self.knowledge = KnowledgeRegistry()
|
|
158
|
+
self.xids = XidRegistry()
|
|
159
|
+
|
|
160
|
+
def add_package(self, manifest: PackageManifest, root: str | Path) -> PackageRecord:
|
|
161
|
+
root = Path(root)
|
|
162
|
+
package = self.packages.add(manifest, root)
|
|
163
|
+
package_assets = (
|
|
164
|
+
("skill", manifest.provides.skills),
|
|
165
|
+
("fragment", manifest.provides.fragments),
|
|
166
|
+
("knowledge", manifest.provides.knowledge),
|
|
167
|
+
("review_axis", manifest.provides.review_axes),
|
|
168
|
+
("schema", manifest.provides.schemas),
|
|
169
|
+
("template", manifest.provides.templates),
|
|
170
|
+
)
|
|
171
|
+
for asset_type, assets in package_assets:
|
|
172
|
+
for asset in assets:
|
|
173
|
+
self.xids.add(
|
|
174
|
+
asset.xid,
|
|
175
|
+
XidAssetRecord(
|
|
176
|
+
xid=asset.xid,
|
|
177
|
+
path=root / asset.path,
|
|
178
|
+
root=root,
|
|
179
|
+
source_type="package",
|
|
180
|
+
asset_type=asset_type,
|
|
181
|
+
includeable=asset_type == "fragment",
|
|
182
|
+
package_id=manifest.package_id,
|
|
183
|
+
fragment_id=asset.id,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
return package
|
|
187
|
+
|
|
188
|
+
def add_package_skill(self, *, package_id: str, skill: SkillDefinition, path: str | Path, root: str | Path) -> None:
|
|
189
|
+
root = Path(root)
|
|
190
|
+
package = self.packages.require(package_id)
|
|
191
|
+
if package.root != root:
|
|
192
|
+
raise ValueError(f"package root mismatch for {package_id}")
|
|
193
|
+
self.xids.add_if_absent(
|
|
194
|
+
skill.xid,
|
|
195
|
+
XidAssetRecord(
|
|
196
|
+
xid=skill.xid,
|
|
197
|
+
path=Path(path),
|
|
198
|
+
root=root,
|
|
199
|
+
source_type="package",
|
|
200
|
+
asset_type="skill",
|
|
201
|
+
includeable=False,
|
|
202
|
+
package_id=package_id,
|
|
203
|
+
fragment_id=skill.skill_id,
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
self.xids.add_if_absent(
|
|
207
|
+
skill.entry.xid,
|
|
208
|
+
XidAssetRecord(
|
|
209
|
+
xid=skill.entry.xid,
|
|
210
|
+
path=root / skill.entry.path,
|
|
211
|
+
root=root,
|
|
212
|
+
source_type="package",
|
|
213
|
+
asset_type="fragment",
|
|
214
|
+
includeable=False,
|
|
215
|
+
package_id=package_id,
|
|
216
|
+
fragment_id="entry",
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
for fragment in skill.required_fragments:
|
|
220
|
+
self.xids.add_if_absent(
|
|
221
|
+
fragment.xid,
|
|
222
|
+
XidAssetRecord(
|
|
223
|
+
xid=fragment.xid,
|
|
224
|
+
path=root / fragment.path,
|
|
225
|
+
root=root,
|
|
226
|
+
source_type="package",
|
|
227
|
+
asset_type="fragment",
|
|
228
|
+
includeable=False,
|
|
229
|
+
package_id=package_id,
|
|
230
|
+
fragment_id=fragment.id,
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
for branch in skill.branches:
|
|
234
|
+
self.xids.add_if_absent(
|
|
235
|
+
branch.xid,
|
|
236
|
+
XidAssetRecord(
|
|
237
|
+
xid=branch.xid,
|
|
238
|
+
path=root / branch.path,
|
|
239
|
+
root=root,
|
|
240
|
+
source_type="package",
|
|
241
|
+
asset_type="fragment",
|
|
242
|
+
includeable=False,
|
|
243
|
+
package_id=package_id,
|
|
244
|
+
fragment_id=branch.id,
|
|
245
|
+
),
|
|
246
|
+
)
|
|
247
|
+
record = SkillRecord(
|
|
248
|
+
package_id=package_id,
|
|
249
|
+
skill_id=skill.skill_id,
|
|
250
|
+
xid=skill.xid,
|
|
251
|
+
definition=skill,
|
|
252
|
+
path=Path(path),
|
|
253
|
+
root=root,
|
|
254
|
+
source_type="package",
|
|
255
|
+
)
|
|
256
|
+
self.skills.add(record)
|
|
257
|
+
|
|
258
|
+
def add_package_knowledge_from_manifest(self, manifest: PackageManifest, root: str | Path) -> None:
|
|
259
|
+
root = Path(root)
|
|
260
|
+
for asset in manifest.provides.knowledge:
|
|
261
|
+
self.knowledge.add(
|
|
262
|
+
KnowledgeRecord(
|
|
263
|
+
id=asset.id,
|
|
264
|
+
xid=asset.xid,
|
|
265
|
+
path=root / asset.path,
|
|
266
|
+
source_type="package",
|
|
267
|
+
package_id=manifest.package_id,
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def add_local_manifest_assets(self, local_root: str | Path, manifest: LocalManifest) -> None:
|
|
272
|
+
local_root = Path(local_root).resolve()
|
|
273
|
+
local_assets = (
|
|
274
|
+
("knowledge", manifest.mounts.knowledge),
|
|
275
|
+
("template", manifest.mounts.templates),
|
|
276
|
+
("schema", manifest.mounts.schemas),
|
|
277
|
+
("review_axis", manifest.mounts.review_axes),
|
|
278
|
+
)
|
|
279
|
+
for asset_type, assets in local_assets:
|
|
280
|
+
for asset in assets:
|
|
281
|
+
asset_path = _contained_path(local_root, asset.path, label=f"local asset {asset.id}")
|
|
282
|
+
self.xids.add(
|
|
283
|
+
asset.xid,
|
|
284
|
+
XidAssetRecord(
|
|
285
|
+
xid=asset.xid,
|
|
286
|
+
path=asset_path,
|
|
287
|
+
root=local_root,
|
|
288
|
+
source_type="local",
|
|
289
|
+
asset_type=asset_type,
|
|
290
|
+
includeable=False,
|
|
291
|
+
local_id=manifest.local_id,
|
|
292
|
+
fragment_id=asset.id,
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
for asset in manifest.mounts.knowledge:
|
|
296
|
+
asset_path = _contained_path(local_root, asset.path, label=f"local asset {asset.id}")
|
|
297
|
+
self.knowledge.add(
|
|
298
|
+
KnowledgeRecord(
|
|
299
|
+
id=asset.id,
|
|
300
|
+
xid=asset.xid,
|
|
301
|
+
path=asset_path,
|
|
302
|
+
source_type="local",
|
|
303
|
+
local_id=manifest.local_id,
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def add_local_skill(self, *, skill: LocalDomainSkill, path: str | Path, root: str | Path, local_id: str) -> None:
|
|
308
|
+
root = Path(root).resolve()
|
|
309
|
+
path = _contained_path(root, str(path), label=f"local skill {skill.skill_id}")
|
|
310
|
+
self.xids.add(
|
|
311
|
+
skill.xid,
|
|
312
|
+
XidAssetRecord(
|
|
313
|
+
xid=skill.xid,
|
|
314
|
+
path=Path(path),
|
|
315
|
+
root=root,
|
|
316
|
+
source_type="local",
|
|
317
|
+
asset_type="skill",
|
|
318
|
+
includeable=False,
|
|
319
|
+
local_id=local_id,
|
|
320
|
+
fragment_id="local_skill",
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
self.skills.add(
|
|
324
|
+
SkillRecord(
|
|
325
|
+
package_id=None,
|
|
326
|
+
skill_id=skill.skill_id,
|
|
327
|
+
xid=skill.xid,
|
|
328
|
+
definition=skill,
|
|
329
|
+
path=Path(path),
|
|
330
|
+
root=root,
|
|
331
|
+
source_type="local",
|
|
332
|
+
local_id=local_id,
|
|
333
|
+
)
|
|
334
|
+
)
|
xrefkit/resolver.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Effective Skill Bundle resolver for XRefKit v2 MVP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .hashing import sha256_file
|
|
8
|
+
from .models import (
|
|
9
|
+
BundleReferences,
|
|
10
|
+
ConflictEntry,
|
|
11
|
+
DomainKnowledgeCatalogEntry,
|
|
12
|
+
EffectiveSkillBundle,
|
|
13
|
+
LoadReason,
|
|
14
|
+
LoadedTexts,
|
|
15
|
+
LocalDomainSkill,
|
|
16
|
+
SkillDefinition,
|
|
17
|
+
SourceTraceEntry,
|
|
18
|
+
SourceType,
|
|
19
|
+
XidLoadedRef,
|
|
20
|
+
)
|
|
21
|
+
from .registry import SkillRecord, XidAssetRecord, XRefKitRegistry
|
|
22
|
+
from .models.common import version_satisfies
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _split_skill_ref(ref: str) -> tuple[str, str]:
|
|
26
|
+
if "::" not in ref:
|
|
27
|
+
raise ValueError(f"skill ref must use package::skill format: {ref}")
|
|
28
|
+
package_id, skill_id = ref.split("::", 1)
|
|
29
|
+
if not package_id or not skill_id:
|
|
30
|
+
raise ValueError(f"invalid skill ref: {ref}")
|
|
31
|
+
return package_id, skill_id
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _loaded(path: Path, xid: str, reason: LoadReason) -> XidLoadedRef:
|
|
35
|
+
return XidLoadedRef(xid=xid, content_hash=sha256_file(path), load_reason=reason)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _trace(
|
|
39
|
+
*,
|
|
40
|
+
path: Path,
|
|
41
|
+
root: Path,
|
|
42
|
+
xid: str,
|
|
43
|
+
source_type: SourceType,
|
|
44
|
+
package_id: str | None = None,
|
|
45
|
+
local_id: str | None = None,
|
|
46
|
+
fragment_id: str | None = None,
|
|
47
|
+
) -> SourceTraceEntry:
|
|
48
|
+
try:
|
|
49
|
+
rel_path = path.relative_to(root).as_posix()
|
|
50
|
+
except ValueError:
|
|
51
|
+
rel_path = path.as_posix()
|
|
52
|
+
return SourceTraceEntry(
|
|
53
|
+
source_type=source_type,
|
|
54
|
+
xid=xid,
|
|
55
|
+
path=rel_path,
|
|
56
|
+
content_hash=sha256_file(path),
|
|
57
|
+
package_id=package_id,
|
|
58
|
+
local_id=local_id,
|
|
59
|
+
fragment_id=fragment_id,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _trace_asset(asset: XidAssetRecord) -> SourceTraceEntry:
|
|
64
|
+
return _trace(
|
|
65
|
+
path=asset.path,
|
|
66
|
+
root=asset.root,
|
|
67
|
+
xid=asset.xid,
|
|
68
|
+
source_type=SourceType(asset.source_type),
|
|
69
|
+
package_id=asset.package_id,
|
|
70
|
+
local_id=asset.local_id,
|
|
71
|
+
fragment_id=asset.fragment_id,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class EffectiveSkillResolver:
|
|
76
|
+
def __init__(self, registry: XRefKitRegistry) -> None:
|
|
77
|
+
self.registry = registry
|
|
78
|
+
|
|
79
|
+
def resolve_entry(self, skill_id: str) -> EffectiveSkillBundle:
|
|
80
|
+
local_record = self.registry.skills.require(skill_id)
|
|
81
|
+
if not isinstance(local_record.definition, LocalDomainSkill):
|
|
82
|
+
raise ValueError("resolve_entry expects a LocalDomainSkill")
|
|
83
|
+
|
|
84
|
+
local_skill = local_record.definition
|
|
85
|
+
extends = local_skill.xrefkit.extends[0]
|
|
86
|
+
package_id, base_skill_id = _split_skill_ref(extends.ref)
|
|
87
|
+
base_record = self.registry.skills.require(base_skill_id)
|
|
88
|
+
if base_record.package_id != package_id:
|
|
89
|
+
raise ValueError(f"extends package mismatch for {extends.ref}")
|
|
90
|
+
if base_record.xid != extends.xid:
|
|
91
|
+
raise ValueError(f"extends XID mismatch for {extends.ref}")
|
|
92
|
+
if not isinstance(base_record.definition, SkillDefinition):
|
|
93
|
+
raise ValueError("extends target must be a package SkillDefinition")
|
|
94
|
+
|
|
95
|
+
base_skill = base_record.definition
|
|
96
|
+
package = self.registry.packages.require(package_id)
|
|
97
|
+
if not version_satisfies(package.manifest.version, extends.version):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"package version {package.manifest.version} does not satisfy {extends.version}"
|
|
100
|
+
)
|
|
101
|
+
if not package.manifest.contract.can_be_extended_by_local_skill:
|
|
102
|
+
raise ValueError(f"package does not allow local extension: {package_id}")
|
|
103
|
+
if not base_skill.extension_policy.can_be_extended_by_local_skill:
|
|
104
|
+
raise ValueError(f"Skill does not allow local extension: {base_skill.skill_id}")
|
|
105
|
+
loaded_texts = LoadedTexts()
|
|
106
|
+
source_trace: list[SourceTraceEntry] = []
|
|
107
|
+
conflicts: list[ConflictEntry] = []
|
|
108
|
+
|
|
109
|
+
self._load_includes(local_skill, loaded_texts, source_trace, conflicts)
|
|
110
|
+
self._load_base_required_texts(base_record, base_skill, loaded_texts, source_trace)
|
|
111
|
+
self._load_local_skill(local_record, local_skill, loaded_texts, source_trace)
|
|
112
|
+
|
|
113
|
+
required_outputs = self._merge_required_outputs(base_skill, local_skill)
|
|
114
|
+
references = BundleReferences(
|
|
115
|
+
supporting_skill_refs=[use.ref for use in local_skill.xrefkit.uses],
|
|
116
|
+
knowledge=list(dict.fromkeys(base_skill.required_knowledge + (local_skill.xrefkit.injects.knowledge if local_skill.xrefkit.injects else []))),
|
|
117
|
+
templates=list(dict.fromkeys([local_skill.xrefkit.output.template_xid] if local_skill.xrefkit.output and local_skill.xrefkit.output.template_xid else [])),
|
|
118
|
+
schemas=list(dict.fromkeys(base_skill.schemas + ([local_skill.xrefkit.output.schema_xid] if local_skill.xrefkit.output and local_skill.xrefkit.output.schema_xid else []))),
|
|
119
|
+
review_axes=list(dict.fromkeys(base_skill.review_axes + local_skill.xrefkit.review_axes)),
|
|
120
|
+
branches=[branch.xid for branch in base_skill.branches],
|
|
121
|
+
)
|
|
122
|
+
available_domain_knowledge = self._build_available_domain_knowledge(set(references.knowledge))
|
|
123
|
+
|
|
124
|
+
return EffectiveSkillBundle(
|
|
125
|
+
effective_skill_id=local_skill.skill_id,
|
|
126
|
+
resolution_mode="entry",
|
|
127
|
+
base_contracts=[extends.ref],
|
|
128
|
+
loaded_texts=loaded_texts,
|
|
129
|
+
references=references,
|
|
130
|
+
available_domain_knowledge=available_domain_knowledge,
|
|
131
|
+
branches_available=[
|
|
132
|
+
{
|
|
133
|
+
"id": branch.id,
|
|
134
|
+
"xid": branch.xid,
|
|
135
|
+
"load_policy": branch.load_policy,
|
|
136
|
+
"condition_summary": ", ".join(branch.condition.any_intent),
|
|
137
|
+
}
|
|
138
|
+
for branch in base_skill.branches
|
|
139
|
+
],
|
|
140
|
+
required_outputs=required_outputs,
|
|
141
|
+
load_policy_applied="entry",
|
|
142
|
+
source_trace=source_trace,
|
|
143
|
+
conflicts=conflicts,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _load_includes(
|
|
147
|
+
self,
|
|
148
|
+
local_skill: LocalDomainSkill,
|
|
149
|
+
loaded_texts: LoadedTexts,
|
|
150
|
+
source_trace: list[SourceTraceEntry],
|
|
151
|
+
conflicts: list[ConflictEntry],
|
|
152
|
+
) -> None:
|
|
153
|
+
seen: set[str] = set()
|
|
154
|
+
for include in local_skill.xrefkit.includes:
|
|
155
|
+
if include.xid in seen:
|
|
156
|
+
conflicts.append(
|
|
157
|
+
ConflictEntry(
|
|
158
|
+
severity="info",
|
|
159
|
+
code="include_skipped_duplicate",
|
|
160
|
+
message=f"duplicate include skipped: {include.xid}",
|
|
161
|
+
xids=[include.xid],
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
seen.add(include.xid)
|
|
166
|
+
asset = self.registry.xids.require_asset(include.xid)
|
|
167
|
+
if asset.asset_type != "fragment" or not asset.includeable:
|
|
168
|
+
raise ValueError(f"includes only accepts fragment assets; xid={include.xid}, actual asset_type={asset.asset_type}")
|
|
169
|
+
loaded_texts.included.append(_loaded(asset.path, include.xid, LoadReason.INCLUDE_FRAGMENT))
|
|
170
|
+
source_trace.append(_trace_asset(asset))
|
|
171
|
+
|
|
172
|
+
def _load_base_required_texts(
|
|
173
|
+
self,
|
|
174
|
+
base_record: SkillRecord,
|
|
175
|
+
base_skill: SkillDefinition,
|
|
176
|
+
loaded_texts: LoadedTexts,
|
|
177
|
+
source_trace: list[SourceTraceEntry],
|
|
178
|
+
) -> None:
|
|
179
|
+
package_id = base_record.package_id
|
|
180
|
+
if package_id is None:
|
|
181
|
+
raise ValueError("base SkillDefinition must belong to a package")
|
|
182
|
+
|
|
183
|
+
entry_path = base_record.root / base_skill.entry.path
|
|
184
|
+
loaded_texts.inherited.append(_loaded(entry_path, base_skill.entry.xid, LoadReason.SKILL_ENTRY))
|
|
185
|
+
source_trace.append(
|
|
186
|
+
_trace(
|
|
187
|
+
path=entry_path,
|
|
188
|
+
root=base_record.root,
|
|
189
|
+
xid=base_skill.entry.xid,
|
|
190
|
+
source_type=SourceType.PACKAGE,
|
|
191
|
+
package_id=package_id,
|
|
192
|
+
fragment_id="entry",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
for fragment in base_skill.required_fragments:
|
|
197
|
+
fragment_path = base_record.root / fragment.path
|
|
198
|
+
loaded_texts.inherited.append(_loaded(fragment_path, fragment.xid, LoadReason.REQUIRED_FRAGMENT))
|
|
199
|
+
source_trace.append(
|
|
200
|
+
_trace(
|
|
201
|
+
path=fragment_path,
|
|
202
|
+
root=base_record.root,
|
|
203
|
+
xid=fragment.xid,
|
|
204
|
+
source_type=SourceType.PACKAGE,
|
|
205
|
+
package_id=package_id,
|
|
206
|
+
fragment_id=fragment.id,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _load_local_skill(
|
|
211
|
+
self,
|
|
212
|
+
local_record: SkillRecord,
|
|
213
|
+
local_skill: LocalDomainSkill,
|
|
214
|
+
loaded_texts: LoadedTexts,
|
|
215
|
+
source_trace: list[SourceTraceEntry],
|
|
216
|
+
) -> None:
|
|
217
|
+
loaded_texts.local.append(_loaded(local_record.path, local_skill.xid, LoadReason.LOCAL_SKILL))
|
|
218
|
+
source_trace.append(
|
|
219
|
+
_trace(
|
|
220
|
+
path=local_record.path,
|
|
221
|
+
root=local_record.root,
|
|
222
|
+
xid=local_skill.xid,
|
|
223
|
+
source_type=SourceType.LOCAL,
|
|
224
|
+
local_id=local_record.local_id or local_record.root.name,
|
|
225
|
+
fragment_id="local_skill",
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _merge_required_outputs(self, base_skill: SkillDefinition, local_skill: LocalDomainSkill) -> list[str]:
|
|
230
|
+
merged = list(dict.fromkeys(base_skill.required_outputs + local_skill.xrefkit.required_outputs))
|
|
231
|
+
missing = set(base_skill.required_outputs) - set(merged)
|
|
232
|
+
if missing:
|
|
233
|
+
raise ValueError(f"local skill weakened required_outputs: {sorted(missing)}")
|
|
234
|
+
return merged
|
|
235
|
+
|
|
236
|
+
def _build_available_domain_knowledge(self, selected_xids: set[str]) -> list[DomainKnowledgeCatalogEntry]:
|
|
237
|
+
entries: list[DomainKnowledgeCatalogEntry] = []
|
|
238
|
+
for record in self.registry.knowledge.list():
|
|
239
|
+
selected = record.xid in selected_xids
|
|
240
|
+
entries.append(
|
|
241
|
+
DomainKnowledgeCatalogEntry(
|
|
242
|
+
id=record.id,
|
|
243
|
+
xid=record.xid,
|
|
244
|
+
source_type=SourceType(record.source_type),
|
|
245
|
+
content_hash=sha256_file(record.path),
|
|
246
|
+
selected=selected,
|
|
247
|
+
package_id=record.package_id,
|
|
248
|
+
local_id=record.local_id,
|
|
249
|
+
selection_reason="referenced_by_effective_skill" if selected else None,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
return entries
|