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.
Files changed (65) hide show
  1. xrefkit/__init__.py +5 -0
  2. xrefkit/__main__.py +5 -0
  3. xrefkit/catalog_cli.py +57 -0
  4. xrefkit/cli.py +71 -0
  5. xrefkit/contracts.py +297 -0
  6. xrefkit/ctx.py +160 -0
  7. xrefkit/dashboard.py +1220 -0
  8. xrefkit/discovery.py +85 -0
  9. xrefkit/gate.py +428 -0
  10. xrefkit/goalstate.py +555 -0
  11. xrefkit/hashing.py +18 -0
  12. xrefkit/import_skill.py +469 -0
  13. xrefkit/instance.py +133 -0
  14. xrefkit/loaders.py +77 -0
  15. xrefkit/mcp/__init__.py +26 -0
  16. xrefkit/mcp/audit.py +168 -0
  17. xrefkit/mcp/bootstrap.py +337 -0
  18. xrefkit/mcp/catalog.py +2638 -0
  19. xrefkit/mcp/cli.py +173 -0
  20. xrefkit/mcp/client_cache.py +356 -0
  21. xrefkit/mcp/context_registry.py +277 -0
  22. xrefkit/mcp/contracts.py +243 -0
  23. xrefkit/mcp/dist.py +234 -0
  24. xrefkit/mcp/ownership.py +246 -0
  25. xrefkit/mcp/repository.py +217 -0
  26. xrefkit/mcp/schemas.py +349 -0
  27. xrefkit/mcp/server.py +773 -0
  28. xrefkit/mcp/startup_contract_pack.py +154 -0
  29. xrefkit/mcp_tools.py +47 -0
  30. xrefkit/models/__init__.py +41 -0
  31. xrefkit/models/common.py +185 -0
  32. xrefkit/models/effective_bundle.py +131 -0
  33. xrefkit/models/local_manifest.py +217 -0
  34. xrefkit/models/package_manifest.py +126 -0
  35. xrefkit/models/run_log.py +276 -0
  36. xrefkit/models/server_config.py +160 -0
  37. xrefkit/models/skill_definition.py +131 -0
  38. xrefkit/operations_cli.py +670 -0
  39. xrefkit/ownership.py +276 -0
  40. xrefkit/packmeta.py +289 -0
  41. xrefkit/registry.py +334 -0
  42. xrefkit/resolver.py +252 -0
  43. xrefkit/resource_provider.py +187 -0
  44. xrefkit/resources/base/contracts.json +178 -0
  45. xrefkit/resources/base/current.json +6 -0
  46. xrefkit/resources/base/generations/7a682a5272907354/contracts.json +178 -0
  47. xrefkit/resources/base/generations/7a682a5272907354/model_body.md +22 -0
  48. xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +178 -0
  49. xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +22 -0
  50. xrefkit/resources/base/model_body.md +22 -0
  51. xrefkit/runlog.py +45 -0
  52. xrefkit/skillmeta.py +1034 -0
  53. xrefkit/skillrun.py +2381 -0
  54. xrefkit/structure_catalog.py +199 -0
  55. xrefkit/tools/__init__.py +119 -0
  56. xrefkit/tools/__main__.py +4 -0
  57. xrefkit/v2_cli.py +130 -0
  58. xrefkit/workspace.py +117 -0
  59. xrefkit/xref.py +1048 -0
  60. xrefkit-0.3.0.dist-info/METADATA +203 -0
  61. xrefkit-0.3.0.dist-info/RECORD +65 -0
  62. xrefkit-0.3.0.dist-info/WHEEL +5 -0
  63. xrefkit-0.3.0.dist-info/entry_points.txt +2 -0
  64. xrefkit-0.3.0.dist-info/licenses/LICENSE +21 -0
  65. 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