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/mcp/catalog.py
ADDED
|
@@ -0,0 +1,2638 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import zipfile
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .contracts import builtin_tool_contracts
|
|
12
|
+
from .ownership import Ownership, load_ownership, validate_ownership
|
|
13
|
+
from .repository import (
|
|
14
|
+
first_heading,
|
|
15
|
+
first_paragraph,
|
|
16
|
+
first_xid,
|
|
17
|
+
file_last_modified,
|
|
18
|
+
markdown_xid_link_targets,
|
|
19
|
+
markdown_xid_only_text,
|
|
20
|
+
markdown_xid_links,
|
|
21
|
+
parse_meta_bullets,
|
|
22
|
+
read_text,
|
|
23
|
+
relative_to_repo,
|
|
24
|
+
repository_identity,
|
|
25
|
+
scalar_list,
|
|
26
|
+
stable_hash,
|
|
27
|
+
)
|
|
28
|
+
from .schemas import (
|
|
29
|
+
ClientObligation,
|
|
30
|
+
ClientToolDistribution,
|
|
31
|
+
ClientToolFile,
|
|
32
|
+
ClientToolManifestEntry,
|
|
33
|
+
ClientToolPipPackage,
|
|
34
|
+
ClosureContract,
|
|
35
|
+
KnowledgeCatalogEntry,
|
|
36
|
+
SkillCatalogEntry,
|
|
37
|
+
SkillRankResult,
|
|
38
|
+
RuntimeRoleContract,
|
|
39
|
+
StartupContext,
|
|
40
|
+
StartupReference,
|
|
41
|
+
ToolContract,
|
|
42
|
+
XRefDocument,
|
|
43
|
+
)
|
|
44
|
+
from .startup_contract_pack import (
|
|
45
|
+
EMBEDDED_BASED_ON_HASHES,
|
|
46
|
+
STARTUP_CONTRACT_PACK_XID,
|
|
47
|
+
normalize_pack_body,
|
|
48
|
+
normalized_startup_contract_pack_body,
|
|
49
|
+
parse_based_on_hashes,
|
|
50
|
+
parse_pack_version,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
TOKEN_RE = re.compile(r"[A-Za-z0-9_+#.-]+")
|
|
55
|
+
IMPORT_RE = re.compile(r"^\s*(?:from|import)\s+([A-Za-z0-9_.]+)", re.MULTILINE)
|
|
56
|
+
CLIENT_TOOL_PACKAGE_ID = "xrefkit-client-python-tools"
|
|
57
|
+
CLIENT_TOOL_PACKAGE_VERSION = "0.1.0"
|
|
58
|
+
XREFKIT_RUNTIME_PACKAGE_ID = "xrefkit"
|
|
59
|
+
XREFKIT_RUNTIME_VERSION_RE = re.compile(r"__version__\s*=\s*[\"']([^\"']+)[\"']")
|
|
60
|
+
CACHE_MAX_VERSION_PAYLOAD_RATIO = 0.5
|
|
61
|
+
XID_DOCUMENT_SUFFIXES = {".md", ".yaml", ".yml"}
|
|
62
|
+
STARTUP_REFERENCE_DEFINITIONS = [
|
|
63
|
+
(
|
|
64
|
+
"0B5C58B5E5B2",
|
|
65
|
+
"base_control",
|
|
66
|
+
),
|
|
67
|
+
(
|
|
68
|
+
"5A1C8E4D2F90",
|
|
69
|
+
"base_control",
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
"6C0B62D6366A",
|
|
73
|
+
"xref_routing",
|
|
74
|
+
),
|
|
75
|
+
(
|
|
76
|
+
"8A666C1FD121",
|
|
77
|
+
"base_control",
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
"A7F3C92D4E11",
|
|
81
|
+
"base_control",
|
|
82
|
+
),
|
|
83
|
+
(
|
|
84
|
+
"4A423E72D2ED",
|
|
85
|
+
"base_control",
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
STOP_TOKENS = {
|
|
89
|
+
"a",
|
|
90
|
+
"an",
|
|
91
|
+
"and",
|
|
92
|
+
"are",
|
|
93
|
+
"as",
|
|
94
|
+
"by",
|
|
95
|
+
"code",
|
|
96
|
+
"for",
|
|
97
|
+
"in",
|
|
98
|
+
"is",
|
|
99
|
+
"of",
|
|
100
|
+
"or",
|
|
101
|
+
"review",
|
|
102
|
+
"skill",
|
|
103
|
+
"the",
|
|
104
|
+
"to",
|
|
105
|
+
"user",
|
|
106
|
+
"with",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
ROUTING_SYNONYMS = {
|
|
110
|
+
"試験計画": ("test", "planning", "plan", "test_flow"),
|
|
111
|
+
"テスト計画": ("test", "planning", "plan", "test_flow"),
|
|
112
|
+
"試験設計": ("test", "design", "test_flow"),
|
|
113
|
+
"テスト設計": ("test", "design", "test_flow"),
|
|
114
|
+
"試験項目": ("test", "item", "case"),
|
|
115
|
+
"テスト項目": ("test", "item", "case"),
|
|
116
|
+
"試験データ": ("test", "data"),
|
|
117
|
+
"テストデータ": ("test", "data"),
|
|
118
|
+
"試験環境": ("test", "environment"),
|
|
119
|
+
"テスト環境": ("test", "environment"),
|
|
120
|
+
"試験ツール": ("test", "tool"),
|
|
121
|
+
"テストツール": ("test", "tool"),
|
|
122
|
+
"テスト用ツール": ("test", "tool", "local", "domain"),
|
|
123
|
+
"試験用ツール": ("test", "tool", "local", "domain"),
|
|
124
|
+
"テスト用スクリプト": ("test", "script", "helper", "automation", "test_flow"),
|
|
125
|
+
"試験用スクリプト": ("test", "script", "helper", "automation", "test_flow"),
|
|
126
|
+
"スクリプト": ("script", "helper", "automation"),
|
|
127
|
+
"カタログ化": ("catalog", "cataloging", "test_tool_catalog_preparation"),
|
|
128
|
+
"カタログ": ("catalog", "test_tool_catalog_preparation"),
|
|
129
|
+
"用意": ("preparation", "setup"),
|
|
130
|
+
"準備": ("preparation", "setup", "implementation"),
|
|
131
|
+
"実施前": ("pre", "execution", "preparation"),
|
|
132
|
+
"実行前": ("pre", "execution", "preparation"),
|
|
133
|
+
"実行": ("execution", "run"),
|
|
134
|
+
"簡易": ("simplify", "helper", "script"),
|
|
135
|
+
"簡易化": ("simplify", "helper", "script"),
|
|
136
|
+
"証跡": ("evidence", "capture"),
|
|
137
|
+
"根拠": ("basis", "evidence"),
|
|
138
|
+
"トレーサビリティ": ("traceability", "xddp"),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ROUTING_CATEGORY_TERMS = {
|
|
142
|
+
"activity": {
|
|
143
|
+
"analysis",
|
|
144
|
+
"catalog",
|
|
145
|
+
"cataloging",
|
|
146
|
+
"design",
|
|
147
|
+
"implementation",
|
|
148
|
+
"execution",
|
|
149
|
+
"planning",
|
|
150
|
+
"preparation",
|
|
151
|
+
"review",
|
|
152
|
+
"run",
|
|
153
|
+
"simplify",
|
|
154
|
+
},
|
|
155
|
+
"artifact": {
|
|
156
|
+
"case",
|
|
157
|
+
"catalog",
|
|
158
|
+
"data",
|
|
159
|
+
"environment",
|
|
160
|
+
"helper",
|
|
161
|
+
"item",
|
|
162
|
+
"plan",
|
|
163
|
+
"script",
|
|
164
|
+
"test",
|
|
165
|
+
"tool",
|
|
166
|
+
},
|
|
167
|
+
"domain": {
|
|
168
|
+
"c#",
|
|
169
|
+
"csharp",
|
|
170
|
+
"database",
|
|
171
|
+
"db",
|
|
172
|
+
"dotnet",
|
|
173
|
+
"release",
|
|
174
|
+
"security",
|
|
175
|
+
"test",
|
|
176
|
+
},
|
|
177
|
+
"phase": {
|
|
178
|
+
"before",
|
|
179
|
+
"execution",
|
|
180
|
+
"implementation",
|
|
181
|
+
"pre",
|
|
182
|
+
"preparation",
|
|
183
|
+
"release",
|
|
184
|
+
"setup",
|
|
185
|
+
},
|
|
186
|
+
"evidence_trace": {
|
|
187
|
+
"basis",
|
|
188
|
+
"evidence",
|
|
189
|
+
"trace",
|
|
190
|
+
"traceability",
|
|
191
|
+
"xddp",
|
|
192
|
+
},
|
|
193
|
+
"tool_runtime": {
|
|
194
|
+
"automation",
|
|
195
|
+
"ci",
|
|
196
|
+
"xrefkit",
|
|
197
|
+
"helper",
|
|
198
|
+
"mcp",
|
|
199
|
+
"script",
|
|
200
|
+
"tool",
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
ROUTING_CATEGORY_WEIGHTS = {
|
|
205
|
+
"activity": 0.5,
|
|
206
|
+
"artifact": 0.7,
|
|
207
|
+
"domain": 0.3,
|
|
208
|
+
"phase": 0.3,
|
|
209
|
+
"evidence_trace": 0.25,
|
|
210
|
+
"tool_runtime": 0.25,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass(frozen=True)
|
|
215
|
+
class XRefCatalog:
|
|
216
|
+
repo_root: Path
|
|
217
|
+
repository_fingerprint: str
|
|
218
|
+
fingerprint_basis: str
|
|
219
|
+
tools: list[ToolContract]
|
|
220
|
+
ownership: Ownership | None = None
|
|
221
|
+
domain_knowledge_roots: tuple[Path, ...] = ()
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def build(
|
|
225
|
+
cls,
|
|
226
|
+
repo_root: str | Path,
|
|
227
|
+
domain_knowledge_roots: list[str | Path] | tuple[str | Path, ...] | None = None,
|
|
228
|
+
) -> "XRefCatalog":
|
|
229
|
+
root = Path(repo_root).resolve()
|
|
230
|
+
if not root.exists():
|
|
231
|
+
raise FileNotFoundError(root)
|
|
232
|
+
external_roots = tuple(
|
|
233
|
+
_resolve_domain_knowledge_root(path) for path in (domain_knowledge_roots or [])
|
|
234
|
+
)
|
|
235
|
+
fingerprint, fingerprint_basis = repository_identity(root)
|
|
236
|
+
ownership = load_ownership(root)
|
|
237
|
+
if ownership is not None:
|
|
238
|
+
errors = validate_ownership(root, ownership)
|
|
239
|
+
if errors:
|
|
240
|
+
raise ValueError("invalid ownership.yaml: " + "; ".join(errors))
|
|
241
|
+
return cls(
|
|
242
|
+
repo_root=root,
|
|
243
|
+
repository_fingerprint=fingerprint,
|
|
244
|
+
fingerprint_basis=fingerprint_basis,
|
|
245
|
+
tools=builtin_tool_contracts(),
|
|
246
|
+
ownership=ownership,
|
|
247
|
+
domain_knowledge_roots=external_roots,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# knowledge, skills, and catalog_version are rebuilt from the live
|
|
251
|
+
# repository on every access so every content-bearing response shares one
|
|
252
|
+
# freshness model with get_document_by_xid (live reads). A frozen
|
|
253
|
+
# build-time snapshot previously let expand_knowledge return a stale
|
|
254
|
+
# content_hash next to a live body on a long-running server, silently
|
|
255
|
+
# breaking the client cache protocol, and hid knowledge/Skill files added
|
|
256
|
+
# or removed after startup.
|
|
257
|
+
@property
|
|
258
|
+
def knowledge(self) -> list[KnowledgeCatalogEntry]:
|
|
259
|
+
return [entry for entry, _text in self._scan_knowledge()]
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def skills(self) -> list[SkillCatalogEntry]:
|
|
263
|
+
return _build_skills(self.repo_root, self.ownership)
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def catalog_version(self) -> str:
|
|
267
|
+
version_basis = "\n".join(
|
|
268
|
+
[entry.content_hash for entry in self.knowledge]
|
|
269
|
+
+ [entry.skill_id + entry.summary for entry in self.skills]
|
|
270
|
+
+ [tool.tool_id + tool.version for tool in self.tools]
|
|
271
|
+
+ ([self.ownership.content_hash] if self.ownership else [])
|
|
272
|
+
)
|
|
273
|
+
return stable_hash(version_basis)[:16]
|
|
274
|
+
|
|
275
|
+
def _scan_knowledge(self) -> list[tuple[KnowledgeCatalogEntry, str]]:
|
|
276
|
+
"""One consistent read per file: entry hash and body come from the
|
|
277
|
+
same text, so they can never disagree."""
|
|
278
|
+
entries: list[tuple[KnowledgeCatalogEntry, str]] = []
|
|
279
|
+
for path in _content_files(self.repo_root, self.ownership, "knowledge", "*.md"):
|
|
280
|
+
text = read_text(path)
|
|
281
|
+
entries.append((_knowledge_entry(self.repo_root, self.ownership, path, text), text))
|
|
282
|
+
for root in self.domain_knowledge_roots:
|
|
283
|
+
for path in _external_knowledge_files(root):
|
|
284
|
+
text = read_text(path)
|
|
285
|
+
if not first_xid(text):
|
|
286
|
+
continue
|
|
287
|
+
entries.append((_external_knowledge_entry(root, path, text), text))
|
|
288
|
+
return entries
|
|
289
|
+
|
|
290
|
+
def get_repository_identity(self) -> dict[str, str]:
|
|
291
|
+
return {
|
|
292
|
+
"repository_fingerprint": self.repository_fingerprint,
|
|
293
|
+
"fingerprint_algorithm": "sha256",
|
|
294
|
+
"fingerprint_basis": self.fingerprint_basis,
|
|
295
|
+
"fingerprint_scope": (
|
|
296
|
+
"shared_across_clones"
|
|
297
|
+
if self.fingerprint_basis == "git_root_commits"
|
|
298
|
+
else "local_path_only"
|
|
299
|
+
),
|
|
300
|
+
"cache_namespace": self.repository_fingerprint,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
def list_knowledge_catalog(self, limit: int | None = None) -> list[dict]:
|
|
304
|
+
return [entry.to_dict() for entry in self.knowledge[: limit or None]]
|
|
305
|
+
|
|
306
|
+
def search_knowledge_catalog(self, query: str, limit: int = 10) -> list[dict]:
|
|
307
|
+
return [entry.to_dict() for entry in _rank_entries(query, self.knowledge)[:limit]]
|
|
308
|
+
|
|
309
|
+
def expand_knowledge(self, xid: str) -> dict:
|
|
310
|
+
entry, content = self._knowledge_by_xid(xid)
|
|
311
|
+
return {"entry": entry.to_dict(), "content": content}
|
|
312
|
+
|
|
313
|
+
def build_knowledge_context(self, query: str, limit: int = 5) -> dict:
|
|
314
|
+
scanned = self._scan_knowledge()
|
|
315
|
+
ranked = _rank_entries(query, [entry for entry, _text in scanned])[:limit]
|
|
316
|
+
by_xid = {entry.xid: (entry, text) for entry, text in scanned}
|
|
317
|
+
expanded: list[dict] = []
|
|
318
|
+
missing: list[dict] = []
|
|
319
|
+
seen: set[str] = set()
|
|
320
|
+
for entry in ranked:
|
|
321
|
+
for candidate_xid in [entry.xid, *entry.requires_knowledge]:
|
|
322
|
+
if candidate_xid in seen:
|
|
323
|
+
continue
|
|
324
|
+
seen.add(candidate_xid)
|
|
325
|
+
candidate = by_xid.get(candidate_xid)
|
|
326
|
+
if not candidate:
|
|
327
|
+
missing.append(
|
|
328
|
+
{
|
|
329
|
+
"xid": candidate_xid,
|
|
330
|
+
"reason": "referenced knowledge was not found in the catalog",
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
continue
|
|
334
|
+
candidate_entry, candidate_text = candidate
|
|
335
|
+
expanded.append(
|
|
336
|
+
{"entry": candidate_entry.to_dict(), "content": candidate_text}
|
|
337
|
+
)
|
|
338
|
+
return {"entries": expanded, "missing": missing}
|
|
339
|
+
|
|
340
|
+
def list_skills(
|
|
341
|
+
self,
|
|
342
|
+
limit: int | None = None,
|
|
343
|
+
include_content: bool = False,
|
|
344
|
+
) -> list[dict]:
|
|
345
|
+
# Metadata-only by default: full procedure bodies are lazy-loaded
|
|
346
|
+
# governance content (get_skill), not routing metadata. The old
|
|
347
|
+
# include_content=True default let one ungated call return every
|
|
348
|
+
# SKILL.md body, bypassing both the startup ordering and the
|
|
349
|
+
# body_mode=lazy context policy.
|
|
350
|
+
entries = self.skills[: limit or None]
|
|
351
|
+
results = [entry.to_dict() for entry in entries]
|
|
352
|
+
duplicate_ids = _duplicate_skill_ids(self.skills)
|
|
353
|
+
for result in results:
|
|
354
|
+
if result["skill_id"] in duplicate_ids:
|
|
355
|
+
result.setdefault("zone_metadata", {})["identity_conflict"] = True
|
|
356
|
+
result["zone_metadata"]["conflict_key"] = result["skill_id"]
|
|
357
|
+
result["zone_metadata"]["conflict_paths"] = duplicate_ids[result["skill_id"]]
|
|
358
|
+
if not include_content:
|
|
359
|
+
for entry, result in zip(entries, results, strict=True):
|
|
360
|
+
result["meta_content"] = None
|
|
361
|
+
result["skill_content"] = None
|
|
362
|
+
result["document_versions"] = _skill_document_versions(
|
|
363
|
+
entry,
|
|
364
|
+
self.repo_root,
|
|
365
|
+
self.repository_fingerprint,
|
|
366
|
+
)
|
|
367
|
+
return results
|
|
368
|
+
|
|
369
|
+
def get_skill(
|
|
370
|
+
self,
|
|
371
|
+
skill_id: str,
|
|
372
|
+
known_document_versions: dict[str, str] | None = None,
|
|
373
|
+
) -> dict:
|
|
374
|
+
entry = self._skill_by_id(skill_id)
|
|
375
|
+
result = entry.to_dict()
|
|
376
|
+
result["client_tool_download"] = _client_tool_download_policy(entry)
|
|
377
|
+
result["content_resolution"] = _mcp_content_resolution_policy()
|
|
378
|
+
if known_document_versions is None:
|
|
379
|
+
return result
|
|
380
|
+
|
|
381
|
+
documents: list[dict] = []
|
|
382
|
+
for relative_path in [entry.meta_path, entry.path]:
|
|
383
|
+
path = self.repo_root / relative_path
|
|
384
|
+
text = read_text(path)
|
|
385
|
+
document = _xref_document(path, self.repo_root, text)
|
|
386
|
+
documents.append(
|
|
387
|
+
_conditional_document_response(
|
|
388
|
+
document,
|
|
389
|
+
known_document_versions.get(document.xid),
|
|
390
|
+
self.repository_fingerprint,
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
result["meta_content"] = None
|
|
394
|
+
result["skill_content"] = None
|
|
395
|
+
result["documents"] = documents
|
|
396
|
+
return result
|
|
397
|
+
|
|
398
|
+
def get_skill_requirements(self, skill_id: str) -> dict:
|
|
399
|
+
entry = self._skill_by_id(skill_id)
|
|
400
|
+
return {
|
|
401
|
+
"skill_id": entry.skill_id,
|
|
402
|
+
"required_knowledge": entry.required_knowledge,
|
|
403
|
+
"required_tools": entry.required_tools,
|
|
404
|
+
"client_tool_download": _client_tool_download_policy(entry),
|
|
405
|
+
"content_resolution": _mcp_content_resolution_policy(),
|
|
406
|
+
"closure_contract": entry.closure_contract.to_dict(),
|
|
407
|
+
"meta_path": entry.meta_path,
|
|
408
|
+
"meta_content": entry.meta_content,
|
|
409
|
+
"meta_links": entry.meta_links,
|
|
410
|
+
"skill_doc": entry.path,
|
|
411
|
+
"skill_content": entry.skill_content,
|
|
412
|
+
"skill_links": entry.skill_links,
|
|
413
|
+
"missing": entry.missing,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
def resolve_skill_knowledge(self, skill_id: str) -> dict:
|
|
417
|
+
"""Resolve a Skill's declared ``knowledge_slots`` against the base+local
|
|
418
|
+
unified catalog (design 082 Decision 3 / 084 M5).
|
|
419
|
+
|
|
420
|
+
Each slot declares a need — a ``query`` or a pinned ``bind`` XID — plus
|
|
421
|
+
acceptance metadata (``min``, ``domain``, ``required``). Selection is
|
|
422
|
+
dynamic (ranked over the merged base+local knowledge roots); the slot
|
|
423
|
+
definition stays in the Skill meta. Returns ranked candidates and
|
|
424
|
+
per-slot satisfaction so planning/routing can gate on required slots.
|
|
425
|
+
Empty ``slots`` for a Skill that has not declared any yet.
|
|
426
|
+
"""
|
|
427
|
+
entry = self._skill_by_id(skill_id)
|
|
428
|
+
knowledge = self.knowledge
|
|
429
|
+
by_xid = {item.xid: item for item in knowledge}
|
|
430
|
+
resolved: list[dict] = []
|
|
431
|
+
for slot in entry.knowledge_slots:
|
|
432
|
+
name = slot.get("slot") or slot.get("name")
|
|
433
|
+
bind = slot.get("bind")
|
|
434
|
+
domain = slot.get("domain")
|
|
435
|
+
min_count = _slot_int(slot.get("min"), 0)
|
|
436
|
+
required = _slot_bool(slot.get("required"))
|
|
437
|
+
if bind:
|
|
438
|
+
match = by_xid.get(str(bind))
|
|
439
|
+
query = None
|
|
440
|
+
candidates = [match.to_dict()] if match else []
|
|
441
|
+
else:
|
|
442
|
+
query = str(slot.get("query") or name or "")
|
|
443
|
+
ranked = _rank_entries(query, knowledge)
|
|
444
|
+
if domain:
|
|
445
|
+
ranked = [item for item in ranked if item.domain == domain]
|
|
446
|
+
candidates = [item.to_dict() for item in ranked[: _slot_int(slot.get("limit"), 5)]]
|
|
447
|
+
satisfied = len(candidates) >= max(min_count, 1) if required else True
|
|
448
|
+
resolved.append(
|
|
449
|
+
{
|
|
450
|
+
"slot": name,
|
|
451
|
+
"query": query,
|
|
452
|
+
"bind": str(bind) if bind else None,
|
|
453
|
+
"domain": domain,
|
|
454
|
+
"min": min_count,
|
|
455
|
+
"required": required,
|
|
456
|
+
"candidates": candidates,
|
|
457
|
+
"satisfied": satisfied,
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
return {
|
|
461
|
+
"skill_id": entry.skill_id,
|
|
462
|
+
"slots": resolved,
|
|
463
|
+
"unsatisfied_required": [
|
|
464
|
+
slot["slot"]
|
|
465
|
+
for slot in resolved
|
|
466
|
+
if slot["required"] and not slot["satisfied"]
|
|
467
|
+
],
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
def rank_skills_for_purpose(self, purpose: str, limit: int = 5) -> list[dict]:
|
|
471
|
+
query_tokens = _tokens(purpose)
|
|
472
|
+
query_categories = _routing_categories(query_tokens)
|
|
473
|
+
results: list[SkillRankResult] = []
|
|
474
|
+
available_tools = {tool.tool_id: tool.version for tool in self.tools}
|
|
475
|
+
for skill in self.skills:
|
|
476
|
+
facets: list[str] = []
|
|
477
|
+
score = 0.0
|
|
478
|
+
for label, values, weight in [
|
|
479
|
+
("skill_id", [skill.skill_id], 0.15),
|
|
480
|
+
("intent", skill.intent, 0.25),
|
|
481
|
+
("target", skill.target_artifacts, 0.25),
|
|
482
|
+
("applies_when", skill.applies_when, 0.2),
|
|
483
|
+
("summary", [skill.summary], 0.2),
|
|
484
|
+
("inputs", skill.inputs, 0.1),
|
|
485
|
+
("outputs", skill.outputs, 0.15),
|
|
486
|
+
(
|
|
487
|
+
"knowledge_slots",
|
|
488
|
+
[_knowledge_slot_text(slot) for slot in skill.knowledge_slots],
|
|
489
|
+
0.1,
|
|
490
|
+
),
|
|
491
|
+
# Skill-centric consolidation (084 M4): the triad is the routing
|
|
492
|
+
# vocabulary. Empty for un-migrated skills, so this is additive.
|
|
493
|
+
("capability", [skill.capability], 0.2),
|
|
494
|
+
("tuning", [skill.tuning], 0.2),
|
|
495
|
+
("responsibility", [skill.responsibility], 0.2),
|
|
496
|
+
]:
|
|
497
|
+
matched = _matched_values(query_tokens, values)
|
|
498
|
+
if matched:
|
|
499
|
+
facets.extend(f"{label}={value}" for value in matched[:3])
|
|
500
|
+
score += weight
|
|
501
|
+
score += min(0.1, 0.02 * _overlap_count(query_tokens, matched))
|
|
502
|
+
matched_categories = _matched_routing_categories(skill, query_categories)
|
|
503
|
+
for category, matches in matched_categories.items():
|
|
504
|
+
category_weight = ROUTING_CATEGORY_WEIGHTS.get(category, 0.0)
|
|
505
|
+
score += category_weight
|
|
506
|
+
score += min(0.15, 0.03 * len(matches))
|
|
507
|
+
facets.extend(f"{category}={value}" for value in matches[:3])
|
|
508
|
+
if skill.skill_id in query_tokens:
|
|
509
|
+
score += 0.6
|
|
510
|
+
facets.append(f"skill_id_alias={skill.skill_id}")
|
|
511
|
+
if (
|
|
512
|
+
"activity" in matched_categories
|
|
513
|
+
and "artifact" in matched_categories
|
|
514
|
+
and _has_required_category_coverage(query_categories, matched_categories)
|
|
515
|
+
):
|
|
516
|
+
score += 1.0
|
|
517
|
+
blocked = _matched_values(query_tokens, skill.not_for, use_stop_words=True)
|
|
518
|
+
if blocked:
|
|
519
|
+
facets.extend(f"not_for={value}" for value in blocked[:3])
|
|
520
|
+
score *= 0.75
|
|
521
|
+
score *= _category_coverage_multiplier(query_categories, matched_categories)
|
|
522
|
+
if "roslyn" in query_tokens and "roslyn" in _tokens(
|
|
523
|
+
" ".join([skill.skill_id, skill.summary, *skill.applies_when])
|
|
524
|
+
):
|
|
525
|
+
score += 0.15
|
|
526
|
+
missing_tools = [
|
|
527
|
+
item.get("tool_id", "")
|
|
528
|
+
for item in skill.required_tools
|
|
529
|
+
if item.get("tool_id") and item.get("tool_id") not in available_tools
|
|
530
|
+
]
|
|
531
|
+
# Declared preconditions travel with the ranking so the client can
|
|
532
|
+
# filter to Skills runnable in the current state (084 M4). Empty
|
|
533
|
+
# until metas adopt preconditions; tool-availability stays the only
|
|
534
|
+
# server-known readiness signal.
|
|
535
|
+
readiness = {
|
|
536
|
+
"runnable": not missing_tools,
|
|
537
|
+
"missing_tool_contracts": missing_tools,
|
|
538
|
+
"declared_preconditions": skill.preconditions,
|
|
539
|
+
}
|
|
540
|
+
if score <= 0:
|
|
541
|
+
continue
|
|
542
|
+
results.append(
|
|
543
|
+
SkillRankResult(
|
|
544
|
+
skill_id=skill.skill_id,
|
|
545
|
+
summary=skill.summary,
|
|
546
|
+
maturity=skill.maturity,
|
|
547
|
+
matched_facets=facets,
|
|
548
|
+
matched_categories=matched_categories,
|
|
549
|
+
closure_preview=skill.closure_contract,
|
|
550
|
+
required_knowledge=skill.required_knowledge,
|
|
551
|
+
execution_readiness=readiness,
|
|
552
|
+
score=round(score, 4),
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
results.sort(key=lambda item: item.score, reverse=True)
|
|
556
|
+
return [item.to_dict() for item in results[:limit]]
|
|
557
|
+
|
|
558
|
+
def list_tool_contracts(self) -> list[dict]:
|
|
559
|
+
return [contract.to_dict() for contract in self.tools]
|
|
560
|
+
|
|
561
|
+
def get_client_tool_manifest(self) -> dict:
|
|
562
|
+
return _client_tool_distribution(self.repo_root).to_dict()
|
|
563
|
+
|
|
564
|
+
def get_client_tool_file(self, path: str) -> dict:
|
|
565
|
+
normalized = path.replace("\\", "/")
|
|
566
|
+
for tool_file in _client_tool_files(self.repo_root):
|
|
567
|
+
if tool_file.path == normalized:
|
|
568
|
+
return tool_file.to_dict()
|
|
569
|
+
raise KeyError(f"client tool file not found: {path}")
|
|
570
|
+
|
|
571
|
+
def get_client_tool_bundle(self) -> dict:
|
|
572
|
+
return {
|
|
573
|
+
"distribution": _client_tool_distribution(self.repo_root).to_dict(),
|
|
574
|
+
"files": [file.to_dict() for file in _client_tool_files(self.repo_root)],
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
def get_client_tool_pip_package(self) -> dict:
|
|
578
|
+
return _client_tool_pip_package(self.repo_root).to_dict()
|
|
579
|
+
|
|
580
|
+
def check_client_tool_versions(self, installed: dict[str, str] | None = None) -> dict:
|
|
581
|
+
installed = installed or {}
|
|
582
|
+
expected = {
|
|
583
|
+
CLIENT_TOOL_PACKAGE_ID: CLIENT_TOOL_PACKAGE_VERSION,
|
|
584
|
+
"xrefkit-client-tools": CLIENT_TOOL_PACKAGE_VERSION,
|
|
585
|
+
}
|
|
586
|
+
results: list[dict[str, str | bool]] = []
|
|
587
|
+
overall_ok = True
|
|
588
|
+
for package_id, version in expected.items():
|
|
589
|
+
actual = installed.get(package_id)
|
|
590
|
+
ok = actual == version
|
|
591
|
+
if not ok:
|
|
592
|
+
overall_ok = False
|
|
593
|
+
status = "ok" if ok else "missing" if actual is None else "mismatch"
|
|
594
|
+
results.append(
|
|
595
|
+
{
|
|
596
|
+
"package_id": package_id,
|
|
597
|
+
"expected_version": version,
|
|
598
|
+
"installed_version": actual or "",
|
|
599
|
+
"status": status,
|
|
600
|
+
"ok": ok,
|
|
601
|
+
}
|
|
602
|
+
)
|
|
603
|
+
return {
|
|
604
|
+
"ok": overall_ok,
|
|
605
|
+
"expected": expected,
|
|
606
|
+
"results": results,
|
|
607
|
+
"instructions": [
|
|
608
|
+
"Client should call this after selecting a Skill that declares client-side required_tools.",
|
|
609
|
+
"If ok is false, install the package returned by get_client_tool_pip_package before executing that Skill's client-side tools.",
|
|
610
|
+
],
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
def get_xrefkit_runtime_manifest(self) -> dict:
|
|
614
|
+
return _xrefkit_runtime_distribution(self.repo_root).to_dict()
|
|
615
|
+
|
|
616
|
+
def get_xrefkit_runtime_file(self, path: str) -> dict:
|
|
617
|
+
normalized = path.replace("\\", "/")
|
|
618
|
+
for file in _xrefkit_runtime_files(self.repo_root):
|
|
619
|
+
if file.path == normalized:
|
|
620
|
+
return file.to_dict()
|
|
621
|
+
raise KeyError(f"xrefkit runtime file not found: {path}")
|
|
622
|
+
|
|
623
|
+
def get_xrefkit_runtime_bundle(self) -> dict:
|
|
624
|
+
return {
|
|
625
|
+
"distribution": _xrefkit_runtime_distribution(self.repo_root).to_dict(),
|
|
626
|
+
"files": [file.to_dict() for file in _xrefkit_runtime_files(self.repo_root)],
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
def get_xrefkit_runtime_pip_package(self) -> dict:
|
|
630
|
+
return _xrefkit_runtime_pip_package(self.repo_root).to_dict()
|
|
631
|
+
|
|
632
|
+
def check_xrefkit_runtime_version(self, installed: dict[str, str] | None = None) -> dict:
|
|
633
|
+
installed = installed or {}
|
|
634
|
+
expected = {XREFKIT_RUNTIME_PACKAGE_ID: _xrefkit_runtime_version(self.repo_root)}
|
|
635
|
+
results: list[dict[str, str | bool]] = []
|
|
636
|
+
overall_ok = True
|
|
637
|
+
for package_id, version in expected.items():
|
|
638
|
+
actual = installed.get(package_id)
|
|
639
|
+
ok = actual == version
|
|
640
|
+
if not ok:
|
|
641
|
+
overall_ok = False
|
|
642
|
+
status = "ok" if ok else "missing" if actual is None else "mismatch"
|
|
643
|
+
results.append(
|
|
644
|
+
{
|
|
645
|
+
"package_id": package_id,
|
|
646
|
+
"expected_version": version,
|
|
647
|
+
"installed_version": actual or "",
|
|
648
|
+
"status": status,
|
|
649
|
+
"ok": ok,
|
|
650
|
+
}
|
|
651
|
+
)
|
|
652
|
+
return {
|
|
653
|
+
"ok": overall_ok,
|
|
654
|
+
"expected": expected,
|
|
655
|
+
"results": results,
|
|
656
|
+
"instructions": [
|
|
657
|
+
"Unlike client-side per-Skill tools, fetch and materialize the xrefkit "
|
|
658
|
+
"runtime right after get_startup_context, before any Skill "
|
|
659
|
+
"routing, since Skill execution requires python -m xrefkit skill run "
|
|
660
|
+
"immediately.",
|
|
661
|
+
"If ok is false, install the package returned by "
|
|
662
|
+
"get_xrefkit_runtime_pip_package, or materialize files from "
|
|
663
|
+
"get_xrefkit_runtime_bundle at the xrefkit/ path in the client repository "
|
|
664
|
+
"root, before running python -m xrefkit.",
|
|
665
|
+
],
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
def get_document_by_xid(
|
|
669
|
+
self,
|
|
670
|
+
xid: str,
|
|
671
|
+
known_version: str | None = None,
|
|
672
|
+
) -> dict:
|
|
673
|
+
matches = _managed_markdown_matches_by_xid(self.repo_root, self.ownership, xid)
|
|
674
|
+
matches.extend(_external_markdown_matches_by_xid(self.domain_knowledge_roots, xid))
|
|
675
|
+
if len(matches) > 1:
|
|
676
|
+
return {
|
|
677
|
+
"ok": False,
|
|
678
|
+
"error": "xid_conflict",
|
|
679
|
+
"xid": xid,
|
|
680
|
+
"message": "multiple catalog-visible documents declare this XID; refusing path-order selection",
|
|
681
|
+
"matches": [
|
|
682
|
+
_document_conflict_match(self.repo_root, self.ownership, path, text)
|
|
683
|
+
for path, text in matches
|
|
684
|
+
],
|
|
685
|
+
}
|
|
686
|
+
if len(matches) == 1:
|
|
687
|
+
path, text = matches[0]
|
|
688
|
+
return _conditional_document_response(
|
|
689
|
+
_xref_document_for_catalog(self.repo_root, self.domain_knowledge_roots, path, text),
|
|
690
|
+
known_version,
|
|
691
|
+
self.repository_fingerprint,
|
|
692
|
+
)
|
|
693
|
+
raise KeyError(f"document xid not found: {xid}")
|
|
694
|
+
|
|
695
|
+
def get_startup_context(
|
|
696
|
+
self,
|
|
697
|
+
known_document_versions: dict[str, str] | None = None,
|
|
698
|
+
) -> dict:
|
|
699
|
+
known_document_versions = known_document_versions or {}
|
|
700
|
+
references: list[StartupReference] = []
|
|
701
|
+
missing: list[dict[str, str]] = []
|
|
702
|
+
managed_documents = _managed_markdown_by_xid(self.repo_root, self.ownership)
|
|
703
|
+
for expected_xid, layer in STARTUP_REFERENCE_DEFINITIONS:
|
|
704
|
+
resolved = managed_documents.get(expected_xid)
|
|
705
|
+
if resolved is None:
|
|
706
|
+
missing.append(
|
|
707
|
+
{
|
|
708
|
+
"xid": expected_xid,
|
|
709
|
+
"reason": "startup reference XID not found",
|
|
710
|
+
}
|
|
711
|
+
)
|
|
712
|
+
continue
|
|
713
|
+
path, text = resolved
|
|
714
|
+
rel_path = relative_to_repo(path, self.repo_root)
|
|
715
|
+
document = _xref_document(path, self.repo_root, text)
|
|
716
|
+
known_version = known_document_versions.get(expected_xid)
|
|
717
|
+
not_modified = known_version == document.content_hash
|
|
718
|
+
cache_status = (
|
|
719
|
+
"not_modified"
|
|
720
|
+
if not_modified
|
|
721
|
+
else "modified"
|
|
722
|
+
if known_version
|
|
723
|
+
else "bypassed"
|
|
724
|
+
)
|
|
725
|
+
references.append(
|
|
726
|
+
StartupReference(
|
|
727
|
+
xid=expected_xid,
|
|
728
|
+
title=first_heading(text, Path(rel_path).stem),
|
|
729
|
+
layer=layer, # type: ignore[arg-type]
|
|
730
|
+
required_at_init=True,
|
|
731
|
+
summary=first_paragraph(text),
|
|
732
|
+
content=None,
|
|
733
|
+
links=markdown_xid_link_targets(text),
|
|
734
|
+
content_hash=document.content_hash,
|
|
735
|
+
cache_status=cache_status,
|
|
736
|
+
content_omitted=True,
|
|
737
|
+
included_in_startup_contract_pack=True,
|
|
738
|
+
cache_policy={"cache_recommended": False, "reason": "startup body represented by startup_contract_pack"},
|
|
739
|
+
repository_fingerprint=self.repository_fingerprint,
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
pack_resolved = managed_documents.get(STARTUP_CONTRACT_PACK_XID)
|
|
743
|
+
pack_document_text = pack_resolved[1] if pack_resolved else None
|
|
744
|
+
startup_contract_pack = _startup_contract_pack(references, pack_document_text)
|
|
745
|
+
client_instructions = _client_instructions()
|
|
746
|
+
if startup_contract_pack["stale"]:
|
|
747
|
+
stale_xids = [
|
|
748
|
+
str(item["xid"]) for item in startup_contract_pack["stale_sources"]
|
|
749
|
+
]
|
|
750
|
+
client_instructions = [
|
|
751
|
+
*client_instructions,
|
|
752
|
+
"startup_contract_pack is STALE: the source documents "
|
|
753
|
+
f"{', '.join(stale_xids)} changed after the pack was authored "
|
|
754
|
+
"(based_on_hashes no longer match source_hashes). Treat the "
|
|
755
|
+
"pack wording as potentially outdated for those areas, resolve "
|
|
756
|
+
"the live sources with get_document_by_xid, and escalate to "
|
|
757
|
+
"the repository maintainers to regenerate the pack.",
|
|
758
|
+
]
|
|
759
|
+
return StartupContext(
|
|
760
|
+
catalog_version=self.catalog_version,
|
|
761
|
+
repository_identity=self.get_repository_identity(),
|
|
762
|
+
access_policy={
|
|
763
|
+
"mode": "mcp_only",
|
|
764
|
+
"source_of_truth": "xrefkit.mcp",
|
|
765
|
+
"applies_to": [
|
|
766
|
+
"startup references",
|
|
767
|
+
"XID-linked Markdown documents",
|
|
768
|
+
"Skill meta and procedure content",
|
|
769
|
+
"workflow definitions",
|
|
770
|
+
"knowledge catalog entries",
|
|
771
|
+
"tool contracts",
|
|
772
|
+
"closure contracts",
|
|
773
|
+
"unknown protocol",
|
|
774
|
+
],
|
|
775
|
+
"forbidden_client_shortcuts": [
|
|
776
|
+
"Do not read XRefKit governance Markdown directly from the client filesystem when this MCP server is configured.",
|
|
777
|
+
"Do not resolve transferred Markdown links by filesystem path.",
|
|
778
|
+
"Do not open local Skill files to bypass get_skill.",
|
|
779
|
+
"Do not interpret path-like response fields such as meta_path, skill_doc, path, or path#xid text as client filesystem fetch instructions.",
|
|
780
|
+
"Do not treat a local checkout as authoritative unless the user explicitly disables MCP-only mode.",
|
|
781
|
+
],
|
|
782
|
+
"required_tools": {
|
|
783
|
+
"cache_identity": "get_repository_identity",
|
|
784
|
+
"startup": "get_startup_context",
|
|
785
|
+
"xid_link_resolution": "get_document_by_xid",
|
|
786
|
+
"skill_content": "get_skill",
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
context_injection_policy=_context_injection_policy(),
|
|
790
|
+
session_context_deduplication=_session_context_deduplication(),
|
|
791
|
+
core_runtime_distribution=_xrefkit_runtime_distribution(self.repo_root).to_dict(),
|
|
792
|
+
repository_zones=_repository_zones(self.ownership),
|
|
793
|
+
client_instructions=client_instructions,
|
|
794
|
+
client_obligations=_client_obligations(),
|
|
795
|
+
link_resolution={
|
|
796
|
+
"link_field": "links",
|
|
797
|
+
"xid_field": "xid",
|
|
798
|
+
"resolver_tool": "get_document_by_xid",
|
|
799
|
+
"resolver_argument": "xid",
|
|
800
|
+
"version_field": "content_hash",
|
|
801
|
+
"conditional_argument": "known_version",
|
|
802
|
+
"path_handling": "server_side_identity_or_diagnostic_only",
|
|
803
|
+
"client_filesystem_resolution": "forbidden",
|
|
804
|
+
"example_call": "get_document_by_xid({\"xid\": \"8A666C1FD121\"})",
|
|
805
|
+
},
|
|
806
|
+
load_order=[reference.xid for reference in references],
|
|
807
|
+
startup_contract_pack=startup_contract_pack,
|
|
808
|
+
references=references,
|
|
809
|
+
semantic_routing_references=_semantic_routing_references(),
|
|
810
|
+
missing=missing,
|
|
811
|
+
).to_dict()
|
|
812
|
+
|
|
813
|
+
def _knowledge_by_xid(self, xid: str) -> tuple[KnowledgeCatalogEntry, str]:
|
|
814
|
+
matches: list[tuple[KnowledgeCatalogEntry, str]] = []
|
|
815
|
+
for entry, text in self._scan_knowledge():
|
|
816
|
+
if entry.xid == xid:
|
|
817
|
+
matches.append((entry, text))
|
|
818
|
+
if len(matches) > 1:
|
|
819
|
+
raise ValueError(
|
|
820
|
+
"knowledge xid conflict: "
|
|
821
|
+
f"{xid} appears in multiple catalog-visible knowledge entries"
|
|
822
|
+
)
|
|
823
|
+
if len(matches) == 1:
|
|
824
|
+
return matches[0]
|
|
825
|
+
raise KeyError(f"knowledge xid not found: {xid}")
|
|
826
|
+
|
|
827
|
+
def _skill_by_id(self, skill_id: str) -> SkillCatalogEntry:
|
|
828
|
+
matches = [entry for entry in self.skills if entry.skill_id == skill_id]
|
|
829
|
+
if len(matches) > 1:
|
|
830
|
+
paths = [entry.meta_path for entry in matches]
|
|
831
|
+
raise ValueError(
|
|
832
|
+
"skill identity conflict: "
|
|
833
|
+
f"{skill_id} appears in multiple catalog-visible entries: {paths}"
|
|
834
|
+
)
|
|
835
|
+
if len(matches) == 1:
|
|
836
|
+
return matches[0]
|
|
837
|
+
raise KeyError(f"skill not found: {skill_id}")
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _client_instructions() -> list[str]:
|
|
841
|
+
return [
|
|
842
|
+
"A client may call get_repository_identity as a content-free cache namespace preflight; get_startup_context remains the first governance-content load.",
|
|
843
|
+
"Fetch core_runtime_distribution (get_xrefkit_runtime_bundle or get_xrefkit_runtime_pip_package) immediately after this call, unconditionally. Unlike client_tool_download, this is not gated behind Skill selection: Skill execution requires python -m xrefkit skill run right after a Skill is chosen.",
|
|
844
|
+
"Materialize and apply startup references in load_order before routing task-specific work. Applying a reference means enforcing its operational contract in the client runtime; it does not require injecting the full document body into the model prompt unless context_injection_policy requires it.",
|
|
845
|
+
"MCP-only mode is active: treat this MCP response as the source of truth for XRefKit governance content.",
|
|
846
|
+
"Do not read XRefKit governance Markdown from the client filesystem while MCP-only mode is active.",
|
|
847
|
+
"Do not assume referenced Markdown files exist on the client filesystem.",
|
|
848
|
+
"Treat path-like metadata such as meta_path, skill_doc, path, or path#xid text as server-side identity or diagnostic metadata only; do not open it through the client filesystem for governance content.",
|
|
849
|
+
"Do not automatically load all links from startup references; use links only when the current task actually needs them.",
|
|
850
|
+
"When transferred Markdown content includes links entries, resolve a needed link by calling get_document_by_xid with the link xid.",
|
|
851
|
+
"Use the returned document content as the authoritative text for that XID.",
|
|
852
|
+
"At startup, record the XIDs used for client-side routing, policy, or context-injection decisions in a client-side audit log.",
|
|
853
|
+
"For Skill entries, use skill_content as the procedure body and resolve skill_links through get_document_by_xid when needed.",
|
|
854
|
+
"After python -m xrefkit skill run returns run_id and run_log, call bind_skill_run with run_id and skill_id, then execute the returned client_record_command with <run-log> replaced by run_log before task-specific XID access.",
|
|
855
|
+
"MCP xid.resolved audit events prove server resolution only. After a body is actually injected into model context, record knowledge load with xrefkit skill knowledge --action load; after it supports a judgment or artifact, record --action apply.",
|
|
856
|
+
"Keep client-side XID document cache entries only when cache_policy.cache_recommended is true.",
|
|
857
|
+
"Fetch client-side tool manifests or packages only after a selected Skill declares client-side required_tools.",
|
|
858
|
+
"Send cached content_hash values as known_version or known_document_versions; when cache_status is not_modified, use the locally hash-validated body instead of downloading it again.",
|
|
859
|
+
]
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _content_files(
|
|
863
|
+
root: Path,
|
|
864
|
+
ownership: Ownership | None,
|
|
865
|
+
family: str,
|
|
866
|
+
pattern: str,
|
|
867
|
+
) -> list[Path]:
|
|
868
|
+
paths: list[Path] = []
|
|
869
|
+
base = root / family
|
|
870
|
+
if base.exists():
|
|
871
|
+
paths.extend(
|
|
872
|
+
path
|
|
873
|
+
for path in sorted(base.glob(f"**/{pattern}"))
|
|
874
|
+
if _catalog_enabled(root, ownership, path)
|
|
875
|
+
)
|
|
876
|
+
packs_root = root / "packs"
|
|
877
|
+
if ownership is not None and packs_root.exists():
|
|
878
|
+
paths.extend(
|
|
879
|
+
path
|
|
880
|
+
for path in sorted(packs_root.glob(f"*/{family}/**/{pattern}"))
|
|
881
|
+
if _catalog_enabled(root, ownership, path)
|
|
882
|
+
)
|
|
883
|
+
paths.extend(
|
|
884
|
+
path
|
|
885
|
+
for path in sorted(packs_root.glob(f"local/*/{family}/**/{pattern}"))
|
|
886
|
+
if _catalog_enabled(root, ownership, path)
|
|
887
|
+
)
|
|
888
|
+
return sorted(set(paths))
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _catalog_enabled(root: Path, ownership: Ownership | None, path: Path) -> bool:
|
|
892
|
+
if ownership is None:
|
|
893
|
+
return True
|
|
894
|
+
return ownership.catalog_enabled(relative_to_repo(path, root))
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def _zone_metadata(ownership: Ownership | None, rel_path: str) -> dict[str, object]:
|
|
898
|
+
if ownership is None:
|
|
899
|
+
return {
|
|
900
|
+
"ownership_enabled": False,
|
|
901
|
+
"zone": None,
|
|
902
|
+
"owner": None,
|
|
903
|
+
"pack_id": None,
|
|
904
|
+
"local_only": False,
|
|
905
|
+
"catalog": True,
|
|
906
|
+
"distribution": True,
|
|
907
|
+
"shadowing": False,
|
|
908
|
+
}
|
|
909
|
+
metadata = ownership.metadata_for(rel_path)
|
|
910
|
+
metadata["ownership_enabled"] = True
|
|
911
|
+
return metadata
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _knowledge_entry(
|
|
915
|
+
root: Path,
|
|
916
|
+
ownership: Ownership | None,
|
|
917
|
+
path: Path,
|
|
918
|
+
text: str,
|
|
919
|
+
) -> KnowledgeCatalogEntry:
|
|
920
|
+
xid = first_xid(text)
|
|
921
|
+
missing: list[str] = []
|
|
922
|
+
if not xid:
|
|
923
|
+
xid = f"path:{relative_to_repo(path, root)}"
|
|
924
|
+
missing.append("xid")
|
|
925
|
+
rel = relative_to_repo(path, root)
|
|
926
|
+
parts = Path(rel).parts
|
|
927
|
+
domain = parts[1] if len(parts) > 2 else "knowledge"
|
|
928
|
+
links = markdown_xid_links(text)
|
|
929
|
+
return KnowledgeCatalogEntry(
|
|
930
|
+
xid=xid,
|
|
931
|
+
version=1,
|
|
932
|
+
content_hash=stable_hash(text),
|
|
933
|
+
revised_at=file_last_modified(path),
|
|
934
|
+
title=first_heading(text, path.stem),
|
|
935
|
+
domain=domain,
|
|
936
|
+
summary=first_paragraph(text),
|
|
937
|
+
applies_when=[],
|
|
938
|
+
requires_knowledge=links,
|
|
939
|
+
related_skills=[],
|
|
940
|
+
related_capabilities=[],
|
|
941
|
+
path=rel,
|
|
942
|
+
missing=missing,
|
|
943
|
+
zone_metadata=_zone_metadata(ownership, rel),
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _external_knowledge_entry(root: Path, path: Path, text: str) -> KnowledgeCatalogEntry:
|
|
948
|
+
xid = first_xid(text)
|
|
949
|
+
if not xid:
|
|
950
|
+
raise ValueError(f"external domain knowledge must declare an XID: {path}")
|
|
951
|
+
rel = _external_relative_path(root, path)
|
|
952
|
+
parts = Path(rel).parts
|
|
953
|
+
domain = parts[0] if len(parts) > 1 else "external_domain_knowledge"
|
|
954
|
+
logical_path = f"external-domain-knowledge/{stable_hash(str(root))[:12]}/{rel}"
|
|
955
|
+
return KnowledgeCatalogEntry(
|
|
956
|
+
xid=xid,
|
|
957
|
+
version=1,
|
|
958
|
+
content_hash=stable_hash(text),
|
|
959
|
+
revised_at=file_last_modified(path),
|
|
960
|
+
title=first_heading(text, path.stem),
|
|
961
|
+
domain=domain,
|
|
962
|
+
summary=first_paragraph(text),
|
|
963
|
+
applies_when=[],
|
|
964
|
+
requires_knowledge=markdown_xid_links(text),
|
|
965
|
+
related_skills=[],
|
|
966
|
+
related_capabilities=[],
|
|
967
|
+
path=logical_path,
|
|
968
|
+
missing=[],
|
|
969
|
+
zone_metadata={
|
|
970
|
+
"ownership_enabled": False,
|
|
971
|
+
"zone": "external_domain_knowledge",
|
|
972
|
+
"owner": None,
|
|
973
|
+
"pack_id": None,
|
|
974
|
+
"local_only": False,
|
|
975
|
+
"catalog": True,
|
|
976
|
+
"distribution": True,
|
|
977
|
+
"shadowing": False,
|
|
978
|
+
},
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _resolve_domain_knowledge_root(path: str | Path) -> Path:
|
|
983
|
+
root = Path(path).expanduser().resolve()
|
|
984
|
+
if not root.exists():
|
|
985
|
+
raise FileNotFoundError(root)
|
|
986
|
+
if not root.is_dir():
|
|
987
|
+
raise NotADirectoryError(root)
|
|
988
|
+
return root
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def _external_knowledge_files(root: Path) -> list[Path]:
|
|
992
|
+
return sorted(path for path in root.glob("**/*.md") if path.is_file())
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def _external_relative_path(root: Path, path: Path) -> str:
|
|
996
|
+
return path.resolve().relative_to(root).as_posix()
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _managed_markdown_files(root: Path, ownership: Ownership | None = None) -> list[Path]:
|
|
1000
|
+
files: list[Path] = []
|
|
1001
|
+
for dirname in ["agent", "docs", "knowledge", "skills"]:
|
|
1002
|
+
base = root / dirname
|
|
1003
|
+
if base.exists():
|
|
1004
|
+
files.extend(
|
|
1005
|
+
path
|
|
1006
|
+
for path in sorted(base.glob("**/*"))
|
|
1007
|
+
if path.is_file()
|
|
1008
|
+
and path.suffix.lower() in XID_DOCUMENT_SUFFIXES
|
|
1009
|
+
and _catalog_enabled(root, ownership, path)
|
|
1010
|
+
)
|
|
1011
|
+
packs_root = root / "packs"
|
|
1012
|
+
if ownership is not None and packs_root.exists():
|
|
1013
|
+
files.extend(
|
|
1014
|
+
path
|
|
1015
|
+
for path in sorted(packs_root.glob("*/**/*"))
|
|
1016
|
+
if path.is_file()
|
|
1017
|
+
and path.suffix.lower() in XID_DOCUMENT_SUFFIXES
|
|
1018
|
+
and _catalog_enabled(root, ownership, path)
|
|
1019
|
+
)
|
|
1020
|
+
return files
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def _managed_markdown_by_xid(root: Path, ownership: Ownership | None = None) -> dict[str, tuple[Path, str]]:
|
|
1024
|
+
documents: dict[str, tuple[Path, str]] = {}
|
|
1025
|
+
for path in _managed_markdown_files(root, ownership):
|
|
1026
|
+
text = read_text(path)
|
|
1027
|
+
xid = first_xid(text)
|
|
1028
|
+
if xid:
|
|
1029
|
+
previous = documents.get(xid)
|
|
1030
|
+
if previous is not None:
|
|
1031
|
+
raise ValueError(
|
|
1032
|
+
f"duplicate catalog-visible XID {xid}: "
|
|
1033
|
+
f"{relative_to_repo(previous[0], root)} and {relative_to_repo(path, root)}"
|
|
1034
|
+
)
|
|
1035
|
+
documents[xid] = (path, text)
|
|
1036
|
+
return documents
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _managed_markdown_matches_by_xid(
|
|
1040
|
+
root: Path,
|
|
1041
|
+
ownership: Ownership | None,
|
|
1042
|
+
xid: str,
|
|
1043
|
+
) -> list[tuple[Path, str]]:
|
|
1044
|
+
matches: list[tuple[Path, str]] = []
|
|
1045
|
+
for path in _managed_markdown_files(root, ownership):
|
|
1046
|
+
text = read_text(path)
|
|
1047
|
+
if first_xid(text) == xid:
|
|
1048
|
+
matches.append((path, text))
|
|
1049
|
+
return matches
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _external_markdown_matches_by_xid(
|
|
1053
|
+
roots: tuple[Path, ...],
|
|
1054
|
+
xid: str,
|
|
1055
|
+
) -> list[tuple[Path, str]]:
|
|
1056
|
+
matches: list[tuple[Path, str]] = []
|
|
1057
|
+
for root in roots:
|
|
1058
|
+
for path in _external_knowledge_files(root):
|
|
1059
|
+
text = read_text(path)
|
|
1060
|
+
if first_xid(text) == xid:
|
|
1061
|
+
matches.append((path, text))
|
|
1062
|
+
return matches
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def _duplicate_skill_ids(entries: list[SkillCatalogEntry]) -> dict[str, list[str]]:
|
|
1066
|
+
by_id: dict[str, list[str]] = {}
|
|
1067
|
+
for entry in entries:
|
|
1068
|
+
by_id.setdefault(entry.skill_id, []).append(entry.meta_path)
|
|
1069
|
+
return {skill_id: paths for skill_id, paths in by_id.items() if len(paths) > 1}
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def _xref_document(path: Path, root: Path, text: str) -> XRefDocument:
|
|
1073
|
+
xid = first_xid(text)
|
|
1074
|
+
if not xid:
|
|
1075
|
+
xid = f"path:{relative_to_repo(path, root)}"
|
|
1076
|
+
content = markdown_xid_only_text(text)
|
|
1077
|
+
return XRefDocument(
|
|
1078
|
+
xid=xid,
|
|
1079
|
+
title=first_heading(text, path.stem),
|
|
1080
|
+
path=relative_to_repo(path, root),
|
|
1081
|
+
summary=first_paragraph(text),
|
|
1082
|
+
content=content,
|
|
1083
|
+
links=markdown_xid_link_targets(text),
|
|
1084
|
+
content_hash=stable_hash(content),
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def _external_xref_document(path: Path, text: str) -> XRefDocument:
|
|
1089
|
+
xid = first_xid(text)
|
|
1090
|
+
if not xid:
|
|
1091
|
+
raise ValueError(f"external domain knowledge must declare an XID: {path}")
|
|
1092
|
+
content = markdown_xid_only_text(text)
|
|
1093
|
+
return XRefDocument(
|
|
1094
|
+
xid=xid,
|
|
1095
|
+
title=first_heading(text, path.stem),
|
|
1096
|
+
path=f"external-domain-knowledge/{xid}.md",
|
|
1097
|
+
summary=first_paragraph(text),
|
|
1098
|
+
content=content,
|
|
1099
|
+
links=markdown_xid_link_targets(text),
|
|
1100
|
+
content_hash=stable_hash(content),
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _path_in_roots(path: Path, roots: tuple[Path, ...]) -> bool:
|
|
1105
|
+
resolved = path.resolve()
|
|
1106
|
+
for root in roots:
|
|
1107
|
+
try:
|
|
1108
|
+
resolved.relative_to(root)
|
|
1109
|
+
except ValueError:
|
|
1110
|
+
continue
|
|
1111
|
+
return True
|
|
1112
|
+
return False
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _xref_document_for_catalog(
|
|
1116
|
+
repo_root: Path,
|
|
1117
|
+
external_roots: tuple[Path, ...],
|
|
1118
|
+
path: Path,
|
|
1119
|
+
text: str,
|
|
1120
|
+
) -> XRefDocument:
|
|
1121
|
+
if _path_in_roots(path, external_roots):
|
|
1122
|
+
return _external_xref_document(path, text)
|
|
1123
|
+
return _xref_document(path, repo_root, text)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _document_conflict_match(
|
|
1127
|
+
repo_root: Path,
|
|
1128
|
+
ownership: Ownership | None,
|
|
1129
|
+
path: Path,
|
|
1130
|
+
text: str,
|
|
1131
|
+
) -> dict[str, object]:
|
|
1132
|
+
try:
|
|
1133
|
+
rel = relative_to_repo(path, repo_root)
|
|
1134
|
+
except ValueError:
|
|
1135
|
+
return {
|
|
1136
|
+
"source": "external_domain_knowledge",
|
|
1137
|
+
"content_hash": _external_xref_document(path, text).content_hash,
|
|
1138
|
+
"zone_metadata": {
|
|
1139
|
+
"ownership_enabled": False,
|
|
1140
|
+
"zone": "external_domain_knowledge",
|
|
1141
|
+
"owner": None,
|
|
1142
|
+
"pack_id": None,
|
|
1143
|
+
"local_only": False,
|
|
1144
|
+
"catalog": True,
|
|
1145
|
+
"distribution": True,
|
|
1146
|
+
"shadowing": False,
|
|
1147
|
+
},
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
"source": "repository",
|
|
1151
|
+
"path": rel,
|
|
1152
|
+
"content_hash": _xref_document(path, repo_root, text).content_hash,
|
|
1153
|
+
"zone_metadata": _zone_metadata(ownership, rel),
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _conditional_document_response(
|
|
1158
|
+
document: XRefDocument,
|
|
1159
|
+
known_version: str | None,
|
|
1160
|
+
repository_fingerprint: str,
|
|
1161
|
+
) -> dict:
|
|
1162
|
+
cache_policy = _document_cache_policy(document, repository_fingerprint)
|
|
1163
|
+
if (
|
|
1164
|
+
known_version == document.content_hash
|
|
1165
|
+
and cache_policy["cache_recommended"]
|
|
1166
|
+
):
|
|
1167
|
+
return {
|
|
1168
|
+
"xid": document.xid,
|
|
1169
|
+
"title": document.title,
|
|
1170
|
+
"content_hash": document.content_hash,
|
|
1171
|
+
"repository_fingerprint": repository_fingerprint,
|
|
1172
|
+
"cache_status": "not_modified",
|
|
1173
|
+
"content_omitted": True,
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
result = document.to_dict()
|
|
1177
|
+
result.update(
|
|
1178
|
+
{
|
|
1179
|
+
"repository_fingerprint": repository_fingerprint,
|
|
1180
|
+
"cache_status": (
|
|
1181
|
+
"bypassed"
|
|
1182
|
+
if known_version == document.content_hash
|
|
1183
|
+
else "modified"
|
|
1184
|
+
if known_version
|
|
1185
|
+
else "miss"
|
|
1186
|
+
),
|
|
1187
|
+
"content_omitted": False,
|
|
1188
|
+
"cache_policy": cache_policy,
|
|
1189
|
+
}
|
|
1190
|
+
)
|
|
1191
|
+
return result
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _document_cache_policy(
|
|
1195
|
+
document: XRefDocument,
|
|
1196
|
+
repository_fingerprint: str,
|
|
1197
|
+
) -> dict:
|
|
1198
|
+
full_document = document.to_dict()
|
|
1199
|
+
full_document["repository_fingerprint"] = repository_fingerprint
|
|
1200
|
+
version_request = {
|
|
1201
|
+
"xid": document.xid,
|
|
1202
|
+
"known_version": document.content_hash,
|
|
1203
|
+
}
|
|
1204
|
+
not_modified_response = {
|
|
1205
|
+
"xid": document.xid,
|
|
1206
|
+
"title": document.title,
|
|
1207
|
+
"content_hash": document.content_hash,
|
|
1208
|
+
"repository_fingerprint": repository_fingerprint,
|
|
1209
|
+
"cache_status": "not_modified",
|
|
1210
|
+
"content_omitted": True,
|
|
1211
|
+
}
|
|
1212
|
+
version_payload_bytes = _json_size(version_request) + _json_size(
|
|
1213
|
+
not_modified_response
|
|
1214
|
+
)
|
|
1215
|
+
document_payload_bytes = _json_size(full_document)
|
|
1216
|
+
ratio = (
|
|
1217
|
+
version_payload_bytes / document_payload_bytes
|
|
1218
|
+
if document_payload_bytes
|
|
1219
|
+
else 1.0
|
|
1220
|
+
)
|
|
1221
|
+
return {
|
|
1222
|
+
"cache_recommended": (
|
|
1223
|
+
not document.xid.startswith("path:")
|
|
1224
|
+
and ratio < CACHE_MAX_VERSION_PAYLOAD_RATIO
|
|
1225
|
+
),
|
|
1226
|
+
"version_payload_bytes": version_payload_bytes,
|
|
1227
|
+
"document_payload_bytes": document_payload_bytes,
|
|
1228
|
+
"version_to_document_ratio": round(ratio, 6),
|
|
1229
|
+
"maximum_ratio": CACHE_MAX_VERSION_PAYLOAD_RATIO,
|
|
1230
|
+
"measurement_scope": "application_json_without_mcp_envelope",
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def _json_size(value: object) -> int:
|
|
1235
|
+
return len(
|
|
1236
|
+
json.dumps(
|
|
1237
|
+
value,
|
|
1238
|
+
ensure_ascii=False,
|
|
1239
|
+
separators=(",", ":"),
|
|
1240
|
+
sort_keys=True,
|
|
1241
|
+
).encode("utf-8")
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def _skill_document_versions(
|
|
1246
|
+
entry: SkillCatalogEntry,
|
|
1247
|
+
root: Path,
|
|
1248
|
+
repository_fingerprint: str,
|
|
1249
|
+
) -> list[dict]:
|
|
1250
|
+
versions: list[dict] = []
|
|
1251
|
+
for relative_path, text in [
|
|
1252
|
+
(entry.meta_path, entry.meta_content),
|
|
1253
|
+
(entry.path, entry.skill_content),
|
|
1254
|
+
]:
|
|
1255
|
+
document = _xref_document(root / relative_path, root, text)
|
|
1256
|
+
versions.append(
|
|
1257
|
+
{
|
|
1258
|
+
"xid": document.xid,
|
|
1259
|
+
"path": document.path,
|
|
1260
|
+
"content_hash": document.content_hash,
|
|
1261
|
+
"repository_fingerprint": repository_fingerprint,
|
|
1262
|
+
"cache_policy": _document_cache_policy(
|
|
1263
|
+
document,
|
|
1264
|
+
repository_fingerprint,
|
|
1265
|
+
),
|
|
1266
|
+
}
|
|
1267
|
+
)
|
|
1268
|
+
return versions
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def _startup_contract_pack(
|
|
1272
|
+
references: list[StartupReference],
|
|
1273
|
+
pack_document_text: str | None,
|
|
1274
|
+
) -> dict[str, object]:
|
|
1275
|
+
source_xids = [reference.xid for reference in references]
|
|
1276
|
+
expected_xids = [xid for xid, _layer in STARTUP_REFERENCE_DEFINITIONS]
|
|
1277
|
+
if source_xids != expected_xids:
|
|
1278
|
+
missing = [xid for xid in expected_xids if xid not in source_xids]
|
|
1279
|
+
extra = [xid for xid in source_xids if xid not in expected_xids]
|
|
1280
|
+
raise ValueError(
|
|
1281
|
+
"startup contract pack source XIDs do not match required startup order: "
|
|
1282
|
+
f"missing={missing}, extra={extra}"
|
|
1283
|
+
)
|
|
1284
|
+
source_hashes: dict[str, str] = {}
|
|
1285
|
+
for reference in references:
|
|
1286
|
+
if not reference.content_hash:
|
|
1287
|
+
raise ValueError(f"startup reference missing content_hash: {reference.xid}")
|
|
1288
|
+
source_hashes[reference.xid] = reference.content_hash
|
|
1289
|
+
|
|
1290
|
+
# The pack is a hand-compressed derivation of the source documents, so
|
|
1291
|
+
# it can drift when a source changes. Authoritative body: the pack
|
|
1292
|
+
# document in the served repository (authored and reviewed next to its
|
|
1293
|
+
# sources); fallback: the body embedded in this package. Either way the
|
|
1294
|
+
# based_on hashes recorded at authoring time are compared against the
|
|
1295
|
+
# live source hashes and any mismatch is reported as staleness instead
|
|
1296
|
+
# of being silently served.
|
|
1297
|
+
if pack_document_text is not None:
|
|
1298
|
+
body = normalize_pack_body(markdown_xid_only_text(pack_document_text))
|
|
1299
|
+
based_on_hashes = parse_based_on_hashes(pack_document_text)
|
|
1300
|
+
pack_version = parse_pack_version(pack_document_text) or 1
|
|
1301
|
+
pack_source = "repository_document"
|
|
1302
|
+
pack_doc_xid: str | None = STARTUP_CONTRACT_PACK_XID
|
|
1303
|
+
else:
|
|
1304
|
+
body = normalized_startup_contract_pack_body()
|
|
1305
|
+
based_on_hashes = dict(EMBEDDED_BASED_ON_HASHES)
|
|
1306
|
+
pack_version = 1
|
|
1307
|
+
pack_source = "embedded_fallback"
|
|
1308
|
+
pack_doc_xid = None
|
|
1309
|
+
|
|
1310
|
+
stale_sources: list[dict[str, str | None]] = []
|
|
1311
|
+
for xid in source_xids:
|
|
1312
|
+
based_on = based_on_hashes.get(xid)
|
|
1313
|
+
if based_on != source_hashes[xid]:
|
|
1314
|
+
stale_sources.append(
|
|
1315
|
+
{
|
|
1316
|
+
"xid": xid,
|
|
1317
|
+
"based_on_hash": based_on,
|
|
1318
|
+
"live_hash": source_hashes[xid],
|
|
1319
|
+
}
|
|
1320
|
+
)
|
|
1321
|
+
return {
|
|
1322
|
+
"mode": "required_startup_contract_pack",
|
|
1323
|
+
"pack_version": pack_version,
|
|
1324
|
+
"pack_source": pack_source,
|
|
1325
|
+
"pack_doc_xid": pack_doc_xid,
|
|
1326
|
+
"source_xids": source_xids,
|
|
1327
|
+
"source_hashes": source_hashes,
|
|
1328
|
+
"based_on_hashes": based_on_hashes,
|
|
1329
|
+
"stale": bool(stale_sources),
|
|
1330
|
+
"stale_sources": stale_sources,
|
|
1331
|
+
"pack_hash": stable_hash(body),
|
|
1332
|
+
"body": body,
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
def _build_skill_entry(root: Path, ownership: Ownership | None, meta_path: Path) -> SkillCatalogEntry:
|
|
1337
|
+
text = read_text(meta_path)
|
|
1338
|
+
meta = parse_meta_bullets(text)
|
|
1339
|
+
skill_id = str(meta.get("skill_id") or meta_path.parent.name)
|
|
1340
|
+
skill_doc_value = str(meta.get("skill_doc") or "./SKILL.md")
|
|
1341
|
+
skill_doc = (meta_path.parent / skill_doc_value).resolve()
|
|
1342
|
+
skill_text = read_text(skill_doc) if skill_doc.exists() else ""
|
|
1343
|
+
missing = _missing_skill_fields(meta, skill_doc.exists())
|
|
1344
|
+
knowledge_refs = scalar_list(meta, "knowledge_refs")
|
|
1345
|
+
knowledge_slots = _parse_knowledge_slots(meta)
|
|
1346
|
+
closure = ClosureContract(
|
|
1347
|
+
closure_conditions=scalar_list(meta, "closure")
|
|
1348
|
+
or _section_bullets(skill_text, "Closure"),
|
|
1349
|
+
exit_enum=["completed", "blocked", "needs_input"],
|
|
1350
|
+
handoff_policy=str(meta.get("constraints") or "explicit handoff required"),
|
|
1351
|
+
worklist_policy=str(
|
|
1352
|
+
_nested_value(meta, "os_contract", "worklist_policy") or "required"
|
|
1353
|
+
),
|
|
1354
|
+
)
|
|
1355
|
+
rel_meta = relative_to_repo(meta_path, root)
|
|
1356
|
+
return SkillCatalogEntry(
|
|
1357
|
+
skill_id=skill_id,
|
|
1358
|
+
title=first_heading(skill_text or text, skill_id),
|
|
1359
|
+
summary=str(meta.get("summary") or first_paragraph(skill_text)),
|
|
1360
|
+
maturity=str(meta.get("maturity") or "unknown"),
|
|
1361
|
+
intent=_derive_intent(meta),
|
|
1362
|
+
target_artifacts=_derive_target_artifacts(meta),
|
|
1363
|
+
applies_when=scalar_list(meta, "applies_when")
|
|
1364
|
+
or scalar_list(meta, "use_when"),
|
|
1365
|
+
not_for=scalar_list(meta, "not_for")
|
|
1366
|
+
or _split_constraints(str(meta.get("constraints") or "")),
|
|
1367
|
+
required_knowledge=(
|
|
1368
|
+
[_knowledge_req(item) for item in knowledge_refs]
|
|
1369
|
+
+ [_bind_knowledge_req(slot) for slot in knowledge_slots if slot.get("bind")]
|
|
1370
|
+
),
|
|
1371
|
+
required_tools=[_required_tool(item) for item in scalar_list(meta, "required_tools")],
|
|
1372
|
+
inputs=scalar_list(meta, "input"),
|
|
1373
|
+
outputs=scalar_list(meta, "output"),
|
|
1374
|
+
closure_contract=closure,
|
|
1375
|
+
meta_content=text,
|
|
1376
|
+
meta_links=markdown_xid_link_targets(text),
|
|
1377
|
+
skill_content=skill_text,
|
|
1378
|
+
skill_links=markdown_xid_link_targets(skill_text),
|
|
1379
|
+
path=relative_to_repo(skill_doc, root) if skill_doc.exists() else "",
|
|
1380
|
+
meta_path=rel_meta,
|
|
1381
|
+
context_size=_skill_context_size(
|
|
1382
|
+
text,
|
|
1383
|
+
skill_text,
|
|
1384
|
+
scalar_list(meta, "output"),
|
|
1385
|
+
closure,
|
|
1386
|
+
),
|
|
1387
|
+
# Skill-centric consolidation (083/084): surface the triad and declared
|
|
1388
|
+
# needs as an additive superset. `responsibility` is the new explicit
|
|
1389
|
+
# field that replaces role_responsibilities.executor (which was always a
|
|
1390
|
+
# responsibility, not a role). Empty where a meta has not adopted the new
|
|
1391
|
+
# fields yet; the legacy nested bullets stay opaque in meta_content.
|
|
1392
|
+
capability=str(meta.get("capability") or ""),
|
|
1393
|
+
tuning=str(meta.get("tuning") or ""),
|
|
1394
|
+
responsibility=str(meta.get("responsibility") or ""),
|
|
1395
|
+
preconditions=scalar_list(meta, "preconditions"),
|
|
1396
|
+
knowledge_slots=knowledge_slots,
|
|
1397
|
+
missing=missing,
|
|
1398
|
+
zone_metadata=_zone_metadata(ownership, rel_meta),
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def _slot_int(value: object, default: int = 0) -> int:
|
|
1403
|
+
try:
|
|
1404
|
+
return int(str(value).strip())
|
|
1405
|
+
except (TypeError, ValueError):
|
|
1406
|
+
return default
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def _slot_bool(value: object) -> bool:
|
|
1410
|
+
if isinstance(value, bool):
|
|
1411
|
+
return value
|
|
1412
|
+
return str(value).strip().lower() == "true"
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
def _parse_slot_spec(spec: str) -> dict:
|
|
1416
|
+
"""Parse a compact meta slot spec that the bullet parser can carry.
|
|
1417
|
+
|
|
1418
|
+
Interim markdown form (until the meta schema settles in the XRefKit-side
|
|
1419
|
+
migration): `name=<slot>; query=<text>; domain=<d>; min=<n>; required;
|
|
1420
|
+
bind=<xid>`. Fields are `;`-separated `key=value` pairs (a bare token means
|
|
1421
|
+
`token=true`), so a `query` may contain spaces. `name` normalizes to `slot`.
|
|
1422
|
+
"""
|
|
1423
|
+
slot: dict = {}
|
|
1424
|
+
for field_text in spec.split(";"):
|
|
1425
|
+
field_text = field_text.strip()
|
|
1426
|
+
if not field_text:
|
|
1427
|
+
continue
|
|
1428
|
+
if "=" in field_text:
|
|
1429
|
+
key, _, val = field_text.partition("=")
|
|
1430
|
+
key, val = key.strip(), val.strip()
|
|
1431
|
+
else:
|
|
1432
|
+
key, val = field_text, "true"
|
|
1433
|
+
slot[key] = val
|
|
1434
|
+
if "name" in slot and "slot" not in slot:
|
|
1435
|
+
slot["slot"] = slot.pop("name")
|
|
1436
|
+
return slot
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def _parse_knowledge_slots(meta: dict) -> list[dict]:
|
|
1440
|
+
"""Normalize declared knowledge slots (design 082 Decision 3).
|
|
1441
|
+
|
|
1442
|
+
Slots are the meta-declared knowledge needs that replace static
|
|
1443
|
+
knowledge_refs; each is resolved at runtime against the base+local catalog.
|
|
1444
|
+
Tolerant during the transition: returns [] when a meta has not adopted
|
|
1445
|
+
knowledge_slots yet, parses compact string specs, and passes mapping entries
|
|
1446
|
+
through unchanged.
|
|
1447
|
+
"""
|
|
1448
|
+
value = meta.get("knowledge_slots")
|
|
1449
|
+
if not isinstance(value, list):
|
|
1450
|
+
return []
|
|
1451
|
+
slots: list[dict] = []
|
|
1452
|
+
for item in value:
|
|
1453
|
+
if isinstance(item, dict):
|
|
1454
|
+
slots.append(dict(item))
|
|
1455
|
+
elif isinstance(item, str) and item.strip():
|
|
1456
|
+
slots.append(_parse_slot_spec(item))
|
|
1457
|
+
return slots
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
def _build_skills(root: Path, ownership: Ownership | None = None) -> list[SkillCatalogEntry]:
|
|
1461
|
+
entries: list[SkillCatalogEntry] = []
|
|
1462
|
+
for meta_path in _content_files(root, ownership, "skills", "meta.md"):
|
|
1463
|
+
entries.append(_build_skill_entry(root, ownership, meta_path))
|
|
1464
|
+
return entries
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _client_tool_distribution(root: Path) -> ClientToolDistribution:
|
|
1468
|
+
package_versions = {
|
|
1469
|
+
CLIENT_TOOL_PACKAGE_ID: CLIENT_TOOL_PACKAGE_VERSION,
|
|
1470
|
+
"xrefkit-client-tools": CLIENT_TOOL_PACKAGE_VERSION,
|
|
1471
|
+
}
|
|
1472
|
+
return ClientToolDistribution(
|
|
1473
|
+
package_id=CLIENT_TOOL_PACKAGE_ID,
|
|
1474
|
+
version=CLIENT_TOOL_PACKAGE_VERSION,
|
|
1475
|
+
execution_location="client",
|
|
1476
|
+
server_executes_tools=False,
|
|
1477
|
+
install_layout="write each file to the same relative path under the client-side target repository root",
|
|
1478
|
+
required_package_ids=sorted(package_versions),
|
|
1479
|
+
package_versions=package_versions,
|
|
1480
|
+
file_hash_algorithm="sha256",
|
|
1481
|
+
version_check_tool="check_client_tool_versions",
|
|
1482
|
+
materialization={
|
|
1483
|
+
"source": "xrefkit.mcp",
|
|
1484
|
+
"file_tool": "get_client_tool_file",
|
|
1485
|
+
"bundle_tool": "get_client_tool_bundle",
|
|
1486
|
+
"pip_package_tool": "get_client_tool_pip_package",
|
|
1487
|
+
"run_location": "client",
|
|
1488
|
+
"preserve_relative_paths": True,
|
|
1489
|
+
},
|
|
1490
|
+
update_policy={
|
|
1491
|
+
"check_on_startup": True,
|
|
1492
|
+
"install_when_missing": True,
|
|
1493
|
+
"update_when_version_mismatch": True,
|
|
1494
|
+
"server_executes_tools": False,
|
|
1495
|
+
},
|
|
1496
|
+
files=[
|
|
1497
|
+
ClientToolManifestEntry(
|
|
1498
|
+
path=file.path,
|
|
1499
|
+
kind=file.kind,
|
|
1500
|
+
content_hash=file.content_hash,
|
|
1501
|
+
size_bytes=file.size_bytes,
|
|
1502
|
+
run_hint=file.run_hint,
|
|
1503
|
+
resolver_tool="get_client_tool_file",
|
|
1504
|
+
resolver_argument="path",
|
|
1505
|
+
)
|
|
1506
|
+
for file in _client_tool_files(root)
|
|
1507
|
+
],
|
|
1508
|
+
instructions=[
|
|
1509
|
+
"The MCP server only distributes these files; it must not execute them.",
|
|
1510
|
+
"Install files at their returned relative paths, typically under tools/ in the client-side repository.",
|
|
1511
|
+
"Run Python tools on the client side with the client repository root as the working directory.",
|
|
1512
|
+
"Some tools expect sibling tools modules, so preserve the returned directory layout.",
|
|
1513
|
+
"Some tools call external programs such as git, dotnet, npm, or project-specific commands; satisfy those prerequisites on the client side before execution.",
|
|
1514
|
+
"structure_graph is an analysis/build-side tool, not a baseline "
|
|
1515
|
+
"client dependency: the client consumes its output as findings "
|
|
1516
|
+
"knowledge, not the tool. If a specific Skill needs structure_graph "
|
|
1517
|
+
"client-side, that Skill declares and provisions it (Skill-scoped, "
|
|
1518
|
+
"prompt-supplemented); XRefKit.StructureGraph is not a global client "
|
|
1519
|
+
"requirement.",
|
|
1520
|
+
"This distribution also includes Skill-embedded scripts under skills/**/*.py that a Skill's SKILL.md instructs running directly by relative path (e.g. skills/<id>/scripts/*.py). get_client_tool_pip_package's tools/-only package does not include these; use get_client_tool_file or get_client_tool_bundle for them.",
|
|
1521
|
+
],
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
def _xrefkit_runtime_version(root: Path) -> str:
|
|
1526
|
+
init_path = root / "xrefkit" / "__init__.py"
|
|
1527
|
+
if not init_path.exists():
|
|
1528
|
+
return "0.0.0"
|
|
1529
|
+
match = XREFKIT_RUNTIME_VERSION_RE.search(read_text(init_path))
|
|
1530
|
+
return match.group(1) if match else "0.0.0"
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
def _xrefkit_runtime_distribution(root: Path) -> ClientToolDistribution:
|
|
1534
|
+
version = _xrefkit_runtime_version(root)
|
|
1535
|
+
package_versions = {XREFKIT_RUNTIME_PACKAGE_ID: version}
|
|
1536
|
+
return ClientToolDistribution(
|
|
1537
|
+
package_id=XREFKIT_RUNTIME_PACKAGE_ID,
|
|
1538
|
+
version=version,
|
|
1539
|
+
execution_location="client",
|
|
1540
|
+
server_executes_tools=False,
|
|
1541
|
+
install_layout="write each file to the same relative path under the client-side target repository root",
|
|
1542
|
+
required_package_ids=sorted(package_versions),
|
|
1543
|
+
package_versions=package_versions,
|
|
1544
|
+
file_hash_algorithm="sha256",
|
|
1545
|
+
version_check_tool="check_xrefkit_runtime_version",
|
|
1546
|
+
materialization={
|
|
1547
|
+
"source": "xrefkit.mcp",
|
|
1548
|
+
"file_tool": "get_xrefkit_runtime_file",
|
|
1549
|
+
"bundle_tool": "get_xrefkit_runtime_bundle",
|
|
1550
|
+
"pip_package_tool": "get_xrefkit_runtime_pip_package",
|
|
1551
|
+
"run_location": "client",
|
|
1552
|
+
"preserve_relative_paths": True,
|
|
1553
|
+
},
|
|
1554
|
+
update_policy={
|
|
1555
|
+
"check_on_startup": True,
|
|
1556
|
+
"install_when_missing": True,
|
|
1557
|
+
"update_when_version_mismatch": True,
|
|
1558
|
+
"server_executes_tools": False,
|
|
1559
|
+
"gated_by_skill_selection": False,
|
|
1560
|
+
"fetch_timing": "immediately_after_get_startup_context",
|
|
1561
|
+
},
|
|
1562
|
+
files=[
|
|
1563
|
+
ClientToolManifestEntry(
|
|
1564
|
+
path=file.path,
|
|
1565
|
+
kind=file.kind,
|
|
1566
|
+
content_hash=file.content_hash,
|
|
1567
|
+
size_bytes=file.size_bytes,
|
|
1568
|
+
run_hint=file.run_hint,
|
|
1569
|
+
resolver_tool="get_xrefkit_runtime_file",
|
|
1570
|
+
resolver_argument="path",
|
|
1571
|
+
)
|
|
1572
|
+
for file in _xrefkit_runtime_files(root)
|
|
1573
|
+
],
|
|
1574
|
+
instructions=[
|
|
1575
|
+
"The MCP server only distributes these files; it must not execute them.",
|
|
1576
|
+
"Unlike client_tool_download (tools/), this runtime is not gated "
|
|
1577
|
+
"behind Skill selection: fetch it right after get_startup_context, "
|
|
1578
|
+
"because Skill execution requires python -m xrefkit skill run "
|
|
1579
|
+
"immediately once a Skill is selected.",
|
|
1580
|
+
"Install files at their returned relative paths, at xrefkit/ in the "
|
|
1581
|
+
"client-side repository root.",
|
|
1582
|
+
"Run python -m xrefkit on the client side with the client repository "
|
|
1583
|
+
"root as the working directory.",
|
|
1584
|
+
"This package depends on PyYAML; install it (get_xrefkit_runtime_pip_package "
|
|
1585
|
+
"does this automatically) or ensure PyYAML is already available "
|
|
1586
|
+
"before running python -m xrefkit.",
|
|
1587
|
+
],
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
def _xrefkit_runtime_files(root: Path) -> list[ClientToolFile]:
|
|
1592
|
+
runtime_root = root / "xrefkit"
|
|
1593
|
+
if not runtime_root.exists():
|
|
1594
|
+
return []
|
|
1595
|
+
distributable_suffixes = {".py", ".json", ".yaml", ".yml", ".md"}
|
|
1596
|
+
paths = sorted(
|
|
1597
|
+
path
|
|
1598
|
+
for path in runtime_root.glob("**/*")
|
|
1599
|
+
if path.is_file()
|
|
1600
|
+
and "__pycache__" not in path.parts
|
|
1601
|
+
and path.suffix.lower() in distributable_suffixes
|
|
1602
|
+
)
|
|
1603
|
+
|
|
1604
|
+
result: list[ClientToolFile] = []
|
|
1605
|
+
for path in paths:
|
|
1606
|
+
rel = relative_to_repo(path, root)
|
|
1607
|
+
text = read_text(path)
|
|
1608
|
+
kind = _client_tool_kind(path)
|
|
1609
|
+
result.append(
|
|
1610
|
+
ClientToolFile(
|
|
1611
|
+
path=rel,
|
|
1612
|
+
kind=kind,
|
|
1613
|
+
content=text,
|
|
1614
|
+
content_hash=stable_hash(text),
|
|
1615
|
+
size_bytes=len(text.encode("utf-8")),
|
|
1616
|
+
run_hint="python -m xrefkit" if kind == "python" else None,
|
|
1617
|
+
imports=_python_imports(text) if kind == "python" else [],
|
|
1618
|
+
links=markdown_xid_link_targets(text),
|
|
1619
|
+
)
|
|
1620
|
+
)
|
|
1621
|
+
return result
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def _xrefkit_runtime_pip_package(root: Path) -> ClientToolPipPackage:
|
|
1625
|
+
files = _xrefkit_runtime_files(root)
|
|
1626
|
+
version = _xrefkit_runtime_version(root)
|
|
1627
|
+
package_root = f"{XREFKIT_RUNTIME_PACKAGE_ID}-{version}"
|
|
1628
|
+
buffer = io.BytesIO()
|
|
1629
|
+
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
1630
|
+
_zip_writestr(
|
|
1631
|
+
archive,
|
|
1632
|
+
f"{package_root}/pyproject.toml",
|
|
1633
|
+
_xrefkit_runtime_pyproject(version),
|
|
1634
|
+
)
|
|
1635
|
+
_zip_writestr(
|
|
1636
|
+
archive,
|
|
1637
|
+
f"{package_root}/README.md",
|
|
1638
|
+
_xrefkit_runtime_readme(),
|
|
1639
|
+
)
|
|
1640
|
+
for file in files:
|
|
1641
|
+
_zip_writestr(archive, f"{package_root}/{file.path}", file.content)
|
|
1642
|
+
content = buffer.getvalue()
|
|
1643
|
+
encoded = base64.b64encode(content).decode("ascii")
|
|
1644
|
+
return ClientToolPipPackage(
|
|
1645
|
+
filename=f"{package_root}.zip",
|
|
1646
|
+
package_id=XREFKIT_RUNTIME_PACKAGE_ID,
|
|
1647
|
+
version=version,
|
|
1648
|
+
package_format="zip-sdist",
|
|
1649
|
+
install_command=f"python -m pip install {package_root}.zip",
|
|
1650
|
+
content_base64=encoded,
|
|
1651
|
+
content_hash=hashlib_sha256_bytes(content),
|
|
1652
|
+
size_bytes=len(content),
|
|
1653
|
+
warnings=[
|
|
1654
|
+
"This package installs a top-level xrefkit package; install it in a "
|
|
1655
|
+
"project virtual environment to avoid conflicts with any "
|
|
1656
|
+
"unrelated package named xrefkit.",
|
|
1657
|
+
"The MCP server only distributes the package; python -m xrefkit "
|
|
1658
|
+
"execution is client-side.",
|
|
1659
|
+
],
|
|
1660
|
+
)
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
def _xrefkit_runtime_pyproject(version: str) -> str:
|
|
1664
|
+
return f"""[build-system]
|
|
1665
|
+
requires = ["setuptools>=68"]
|
|
1666
|
+
build-backend = "setuptools.build_meta"
|
|
1667
|
+
|
|
1668
|
+
[project]
|
|
1669
|
+
name = "{XREFKIT_RUNTIME_PACKAGE_ID}"
|
|
1670
|
+
version = "{version}"
|
|
1671
|
+
description = "Portable XRefKit runtime distributed from XRefKit MCP"
|
|
1672
|
+
readme = "README.md"
|
|
1673
|
+
requires-python = ">=3.11"
|
|
1674
|
+
dependencies = ["PyYAML>=6.0.2,<7", "pydantic>=2.0,<3"]
|
|
1675
|
+
|
|
1676
|
+
[tool.setuptools.packages.find]
|
|
1677
|
+
include = ["xrefkit*"]
|
|
1678
|
+
|
|
1679
|
+
[tool.setuptools.package-data]
|
|
1680
|
+
xrefkit = ["resources/**/*"]
|
|
1681
|
+
"""
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
def _xrefkit_runtime_readme() -> str:
|
|
1685
|
+
return """# XRefKit Runtime
|
|
1686
|
+
|
|
1687
|
+
This package is generated by XRefKit MCP and installs the `xrefkit` package that
|
|
1688
|
+
implements XRefKit's Skill-execution runtime (`python -m xrefkit skill run`,
|
|
1689
|
+
`workitem`, `artifact`, `concern`, `phase`, `verify`, `close`) for client-side
|
|
1690
|
+
execution.
|
|
1691
|
+
|
|
1692
|
+
The MCP server does not execute `xrefkit`. Run it in the client-side target
|
|
1693
|
+
repository:
|
|
1694
|
+
|
|
1695
|
+
```powershell
|
|
1696
|
+
python -m xrefkit skill run --meta <path-to-meta.md> --task "<task>" --json
|
|
1697
|
+
```
|
|
1698
|
+
"""
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
def _client_obligations() -> list[ClientObligation]:
|
|
1702
|
+
return [
|
|
1703
|
+
ClientObligation(
|
|
1704
|
+
id="startup.first_call",
|
|
1705
|
+
level="must",
|
|
1706
|
+
applies_when="xrefkit.mcp_configured",
|
|
1707
|
+
statement="Call get_startup_context before task-specific routing.",
|
|
1708
|
+
enforcement_owner="server",
|
|
1709
|
+
verification=(
|
|
1710
|
+
"get_document_by_xid, get_skill, get_skill_requirements, "
|
|
1711
|
+
"expand_knowledge, get_knowledge_summary, "
|
|
1712
|
+
"build_knowledge_context, and list_skills with "
|
|
1713
|
+
"include_content=true reject the call for any MCP session "
|
|
1714
|
+
"that has not first called get_startup_context"
|
|
1715
|
+
),
|
|
1716
|
+
),
|
|
1717
|
+
ClientObligation(
|
|
1718
|
+
id="tools.no_download_before_skill_selection",
|
|
1719
|
+
level="must",
|
|
1720
|
+
applies_when="client considers fetching client-side tool distribution",
|
|
1721
|
+
statement=(
|
|
1722
|
+
"Do not fetch client-tool manifests, files, bundles, or pip "
|
|
1723
|
+
"packages before selecting a Skill via get_skill or "
|
|
1724
|
+
"get_skill_requirements."
|
|
1725
|
+
),
|
|
1726
|
+
enforcement_owner="server",
|
|
1727
|
+
verification=(
|
|
1728
|
+
"get_client_tool_manifest, get_client_tool_file, "
|
|
1729
|
+
"get_client_tool_bundle, and get_client_tool_pip_package reject "
|
|
1730
|
+
"the call for any MCP session that has not first called "
|
|
1731
|
+
"get_skill or get_skill_requirements"
|
|
1732
|
+
),
|
|
1733
|
+
),
|
|
1734
|
+
ClientObligation(
|
|
1735
|
+
id="core_runtime.fetch_immediately",
|
|
1736
|
+
level="must",
|
|
1737
|
+
applies_when="xrefkit.mcp_configured",
|
|
1738
|
+
statement=(
|
|
1739
|
+
"Fetch and materialize the xrefkit runtime from "
|
|
1740
|
+
"core_runtime_distribution right after get_startup_context, "
|
|
1741
|
+
"unconditionally, rather than deferring it the way per-Skill "
|
|
1742
|
+
"client-tool distribution is deferred. Skill execution "
|
|
1743
|
+
"requires python -m xrefkit skill run immediately once a Skill is "
|
|
1744
|
+
"selected, so the runtime must already be available by then."
|
|
1745
|
+
),
|
|
1746
|
+
enforcement_owner="client",
|
|
1747
|
+
verification=(
|
|
1748
|
+
"python -m xrefkit succeeds in the client-side target repository, "
|
|
1749
|
+
"or xrefkit/ files materialized from get_xrefkit_runtime_bundle are "
|
|
1750
|
+
"present, before the client's first get_skill call in the "
|
|
1751
|
+
"session"
|
|
1752
|
+
),
|
|
1753
|
+
),
|
|
1754
|
+
ClientObligation(
|
|
1755
|
+
id="content.mcp_only",
|
|
1756
|
+
level="must",
|
|
1757
|
+
applies_when="access_policy.mode == mcp_only",
|
|
1758
|
+
statement="Do not read XRefKit governance Markdown from a local filesystem checkout.",
|
|
1759
|
+
enforcement_owner="client",
|
|
1760
|
+
verification="XID-linked governance content is obtained through get_document_by_xid or get_skill",
|
|
1761
|
+
),
|
|
1762
|
+
ClientObligation(
|
|
1763
|
+
id="links.resolve_by_xid",
|
|
1764
|
+
level="must",
|
|
1765
|
+
applies_when="transferred content contains links entries",
|
|
1766
|
+
statement="Resolve needed Markdown links by XID through get_document_by_xid.",
|
|
1767
|
+
enforcement_owner="client",
|
|
1768
|
+
verification="link resolver uses resolver_tool and resolver_argument from link metadata",
|
|
1769
|
+
),
|
|
1770
|
+
ClientObligation(
|
|
1771
|
+
id="startup.log_decision_xids",
|
|
1772
|
+
level="must",
|
|
1773
|
+
applies_when="startup context is materialized by the client",
|
|
1774
|
+
statement="Record the startup XIDs used for client-side routing, policy, or context-injection decisions in a client-side audit log.",
|
|
1775
|
+
enforcement_owner="client",
|
|
1776
|
+
verification="client startup audit log contains repository_fingerprint, load_order_xids, startup_contract_pack_source_xids, reference_xids, and client_decision_xids",
|
|
1777
|
+
),
|
|
1778
|
+
ClientObligation(
|
|
1779
|
+
id="tools.materialize_from_mcp",
|
|
1780
|
+
level="must",
|
|
1781
|
+
applies_when="client executes XRefKit-distributed tools",
|
|
1782
|
+
statement="Fetch, materialize or install, and version-check client-side tools from XRefKit MCP before execution.",
|
|
1783
|
+
enforcement_owner="client",
|
|
1784
|
+
verification="check_client_tool_versions passes for the installed client-tool package versions",
|
|
1785
|
+
),
|
|
1786
|
+
ClientObligation(
|
|
1787
|
+
id="tools.client_side_execution",
|
|
1788
|
+
level="must",
|
|
1789
|
+
applies_when="client runs XRefKit-distributed tools",
|
|
1790
|
+
statement="Run distributed tools only in the client execution environment; the MCP server does not execute them.",
|
|
1791
|
+
enforcement_owner="client",
|
|
1792
|
+
verification="tool execution occurs outside the XRefKit MCP server process",
|
|
1793
|
+
),
|
|
1794
|
+
ClientObligation(
|
|
1795
|
+
id="context.no_duplicate_xid_body_per_session",
|
|
1796
|
+
level="must",
|
|
1797
|
+
applies_when="assembling model context",
|
|
1798
|
+
statement="Within a single client session, the client MUST NOT inject more than one full document body for the same repository_fingerprint, xid, and content_hash into the active model context. If the same XID version is needed again, the client MUST reference the existing session context entry by XID and content_hash instead of repeating the body.",
|
|
1799
|
+
enforcement_owner="client",
|
|
1800
|
+
verification="Prompt assembly maintains a session-visible XID index and records injected_xids, reused_xids, content_hash values, visibility status, and reuse reasons for each model turn.",
|
|
1801
|
+
),
|
|
1802
|
+
]
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
def _semantic_routing_references() -> list[dict[str, object]]:
|
|
1806
|
+
return [
|
|
1807
|
+
{
|
|
1808
|
+
"id": "skills",
|
|
1809
|
+
"purpose": "semantic Skill routing from user intent before procedure load",
|
|
1810
|
+
"summary_tool": "list_skills",
|
|
1811
|
+
"summary_arguments": {"include_content": False},
|
|
1812
|
+
"rank_tool": "rank_skills_for_purpose",
|
|
1813
|
+
"materialize_tool": "get_skill",
|
|
1814
|
+
"materialize_argument": "skill_id",
|
|
1815
|
+
"body_mode": "lazy",
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
"id": "knowledge",
|
|
1819
|
+
"purpose": "domain-knowledge search after a task or Skill needs evidence",
|
|
1820
|
+
"summary_tool": "search_knowledge_catalog",
|
|
1821
|
+
"summary_arguments": {"limit": 10},
|
|
1822
|
+
"materialize_tool": "expand_knowledge",
|
|
1823
|
+
"materialize_argument": "xid",
|
|
1824
|
+
"body_mode": "lazy",
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
"id": "tool_contracts",
|
|
1828
|
+
"purpose": "tool capability lookup when a task needs exact tool boundaries",
|
|
1829
|
+
"summary_tool": "list_tool_contracts",
|
|
1830
|
+
"body_mode": "metadata_only",
|
|
1831
|
+
},
|
|
1832
|
+
]
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
def _client_tool_download_policy(entry: SkillCatalogEntry) -> dict[str, object]:
|
|
1836
|
+
required_client_tools = [
|
|
1837
|
+
item
|
|
1838
|
+
for item in entry.required_tools
|
|
1839
|
+
if item.get("execution_location") == "client" or item.get("name")
|
|
1840
|
+
]
|
|
1841
|
+
return {
|
|
1842
|
+
"required": bool(required_client_tools),
|
|
1843
|
+
"required_client_tools": required_client_tools,
|
|
1844
|
+
"download_when": "after this Skill is selected for use and before executing its client-side required_tools",
|
|
1845
|
+
"do_not_download_at_startup": True,
|
|
1846
|
+
"manifest_tool": "get_client_tool_manifest",
|
|
1847
|
+
"package_tool": "get_client_tool_pip_package",
|
|
1848
|
+
"file_tool": "get_client_tool_file",
|
|
1849
|
+
"bundle_tool": "get_client_tool_bundle",
|
|
1850
|
+
"version_check_tool": "check_client_tool_versions",
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
def _skill_context_size(
|
|
1855
|
+
meta_content: str,
|
|
1856
|
+
skill_content: str,
|
|
1857
|
+
outputs: list[str],
|
|
1858
|
+
closure: ClosureContract,
|
|
1859
|
+
) -> dict[str, object]:
|
|
1860
|
+
meta_size = _text_size(meta_content)
|
|
1861
|
+
skill_size = _text_size(skill_content)
|
|
1862
|
+
read_size = _sum_text_sizes([meta_size, skill_size])
|
|
1863
|
+
write_contract_size = _text_size(
|
|
1864
|
+
"\n".join(
|
|
1865
|
+
[
|
|
1866
|
+
*outputs,
|
|
1867
|
+
*closure.closure_conditions,
|
|
1868
|
+
*closure.exit_enum,
|
|
1869
|
+
closure.handoff_policy,
|
|
1870
|
+
closure.worklist_policy,
|
|
1871
|
+
]
|
|
1872
|
+
)
|
|
1873
|
+
)
|
|
1874
|
+
return {
|
|
1875
|
+
"unit": "estimated_tokens",
|
|
1876
|
+
"estimator": "ceil(characters / 4)",
|
|
1877
|
+
"model_tokenizer": None,
|
|
1878
|
+
"read": read_size,
|
|
1879
|
+
"write_contract": write_contract_size,
|
|
1880
|
+
"write_contract_note": "Declared output and closure contract size only; actual generated output tokens are runtime-dependent.",
|
|
1881
|
+
"meta": meta_size,
|
|
1882
|
+
"skill": skill_size,
|
|
1883
|
+
"total": read_size,
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def _text_size(value: str) -> dict[str, int]:
|
|
1888
|
+
characters = len(value)
|
|
1889
|
+
return {
|
|
1890
|
+
"bytes_utf8": len(value.encode("utf-8")),
|
|
1891
|
+
"characters": characters,
|
|
1892
|
+
"estimated_tokens": (characters + 3) // 4,
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
|
|
1896
|
+
def _sum_text_sizes(sizes: list[dict[str, int]]) -> dict[str, int]:
|
|
1897
|
+
return {
|
|
1898
|
+
"bytes_utf8": sum(size["bytes_utf8"] for size in sizes),
|
|
1899
|
+
"characters": sum(size["characters"] for size in sizes),
|
|
1900
|
+
"estimated_tokens": sum(size["estimated_tokens"] for size in sizes),
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
def _workflow_protocol() -> dict[str, object]:
|
|
1905
|
+
return {
|
|
1906
|
+
"source": "xrefkit.mcp",
|
|
1907
|
+
"routing": {
|
|
1908
|
+
"selection_basis": [
|
|
1909
|
+
"user intent",
|
|
1910
|
+
"startup load_order",
|
|
1911
|
+
"workflow catalog",
|
|
1912
|
+
"Skill catalog",
|
|
1913
|
+
"XID-linked evidence resolved through get_document_by_xid",
|
|
1914
|
+
],
|
|
1915
|
+
"workflow_selection": "deterministic catalog metadata once selected; semantic selection may be performed by the client before execution",
|
|
1916
|
+
"skill_selection": "route by catalog metadata, then fetch selected Skill through get_skill",
|
|
1917
|
+
},
|
|
1918
|
+
"phase_order": [
|
|
1919
|
+
"startup",
|
|
1920
|
+
"planning",
|
|
1921
|
+
"execution",
|
|
1922
|
+
"check",
|
|
1923
|
+
"quality",
|
|
1924
|
+
"closure",
|
|
1925
|
+
"handoff",
|
|
1926
|
+
],
|
|
1927
|
+
"role_ownership": {
|
|
1928
|
+
"executor": "Skill-specific execution role",
|
|
1929
|
+
"checker": "protocol-owned deterministic run-record verification",
|
|
1930
|
+
"quality_reviewer": "protocol-owned quality review role separate from executor",
|
|
1931
|
+
"handoff_owner": "protocol-owned handoff role",
|
|
1932
|
+
},
|
|
1933
|
+
"deterministic_checks": [
|
|
1934
|
+
"load_order is returned by get_startup_context",
|
|
1935
|
+
"XID links are resolved only through get_document_by_xid",
|
|
1936
|
+
"check phase is advanced by deterministic xrefkit skill verify semantics",
|
|
1937
|
+
"content identity is content_hash; no duplicate document version field is emitted",
|
|
1938
|
+
],
|
|
1939
|
+
"non_deterministic_decisions": [
|
|
1940
|
+
"semantic workflow or Skill routing from user intent",
|
|
1941
|
+
"quality judgment after deterministic checks pass",
|
|
1942
|
+
"task-specific evidence sufficiency judgment",
|
|
1943
|
+
],
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
def _context_injection_policy() -> dict[str, object]:
|
|
1948
|
+
return {
|
|
1949
|
+
"model_context_format": "plain_text",
|
|
1950
|
+
"model_context_source": "startup_contract_pack.body",
|
|
1951
|
+
"do_not_inject_raw_startup_json": True,
|
|
1952
|
+
"default_document_body_mode": "lazy",
|
|
1953
|
+
"default_nonstartup_document_body_mode": "lazy",
|
|
1954
|
+
"startup_reference_prompt_mode": "required_startup_contract_pack",
|
|
1955
|
+
"startup_contract_pack_visible_by_default": True,
|
|
1956
|
+
"startup_reference_body_visible_by_default": False,
|
|
1957
|
+
"materialize_does_not_imply_prompt_injection": True,
|
|
1958
|
+
"body_injection_unit": "xid_document",
|
|
1959
|
+
"body_visible_by_default": False,
|
|
1960
|
+
"metadata_visible_by_default": [
|
|
1961
|
+
"xid",
|
|
1962
|
+
"title",
|
|
1963
|
+
"summary",
|
|
1964
|
+
"layer",
|
|
1965
|
+
"required_at_init",
|
|
1966
|
+
"content_hash",
|
|
1967
|
+
"links",
|
|
1968
|
+
"cache_status",
|
|
1969
|
+
"client_cache_status",
|
|
1970
|
+
],
|
|
1971
|
+
"inject_body_when": [
|
|
1972
|
+
"the active task explicitly requires that XID",
|
|
1973
|
+
"the selected workflow or Skill declares that XID as required evidence",
|
|
1974
|
+
"the model requests a linked XID that is needed to resolve a concrete uncertainty",
|
|
1975
|
+
"a closure, safety, or verification check depends on the exact wording of that XID",
|
|
1976
|
+
"the user explicitly asks to inspect, quote, edit, or verify that document",
|
|
1977
|
+
],
|
|
1978
|
+
"do_not_inject_body_when": [
|
|
1979
|
+
"the XID is only present as a related link",
|
|
1980
|
+
"the summary is sufficient for routing",
|
|
1981
|
+
"the document is cached only for future resolution",
|
|
1982
|
+
"the document belongs to a lower-layer context that is not active for the current task",
|
|
1983
|
+
],
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
def _session_context_deduplication() -> dict[str, object]:
|
|
1988
|
+
return {
|
|
1989
|
+
"scope": "single_client_session",
|
|
1990
|
+
"dedupe_key": [
|
|
1991
|
+
"repository_fingerprint",
|
|
1992
|
+
"xid",
|
|
1993
|
+
"content_hash",
|
|
1994
|
+
],
|
|
1995
|
+
"active_model_context_cardinality": "at_most_one_body_per_dedupe_key",
|
|
1996
|
+
"materialize_does_not_imply_duplicate_injection": True,
|
|
1997
|
+
"on_repeated_xid_same_hash": "reference_existing_session_context_entry",
|
|
1998
|
+
"on_repeated_xid_different_hash": "treat_as_version_change_and_replace_or_escalate",
|
|
1999
|
+
"reinject_body_only_when": [
|
|
2000
|
+
"the previous body is no longer visible in the active model context",
|
|
2001
|
+
"the content_hash changed and the new version is selected",
|
|
2002
|
+
"the client intentionally rebuilds the active context after compaction",
|
|
2003
|
+
],
|
|
2004
|
+
"trace_required": True,
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
|
|
2008
|
+
def _repository_zones(ownership: Ownership | None) -> dict[str, object]:
|
|
2009
|
+
if ownership is None:
|
|
2010
|
+
return {
|
|
2011
|
+
"ownership_enabled": False,
|
|
2012
|
+
"ownership_hash": None,
|
|
2013
|
+
"zone_ids": [],
|
|
2014
|
+
"local_packs_declared": False,
|
|
2015
|
+
"catalog_roots_are_zone_aware": False,
|
|
2016
|
+
}
|
|
2017
|
+
zone_ids = [zone.id for zone in ownership.zones]
|
|
2018
|
+
return {
|
|
2019
|
+
"ownership_enabled": True,
|
|
2020
|
+
"ownership_hash": ownership.content_hash,
|
|
2021
|
+
"zone_ids": zone_ids,
|
|
2022
|
+
"local_packs_declared": any(zone.id == "local-packs" for zone in ownership.zones),
|
|
2023
|
+
"catalog_roots_are_zone_aware": True,
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
|
|
2027
|
+
def _client_tool_files(root: Path) -> list[ClientToolFile]:
|
|
2028
|
+
ownership = load_ownership(root)
|
|
2029
|
+
if ownership is not None:
|
|
2030
|
+
errors = validate_ownership(root, ownership)
|
|
2031
|
+
if errors:
|
|
2032
|
+
raise ValueError("invalid ownership.yaml: " + "; ".join(errors))
|
|
2033
|
+
tools_root = root / "tools"
|
|
2034
|
+
paths: list[Path] = []
|
|
2035
|
+
support_paths: list[Path] = []
|
|
2036
|
+
if tools_root.exists():
|
|
2037
|
+
paths.extend(sorted(tools_root.glob("**/*.py")))
|
|
2038
|
+
support_paths.extend(
|
|
2039
|
+
path
|
|
2040
|
+
for path in sorted((tools_root / "profiles").glob("**/*"))
|
|
2041
|
+
if path.is_file()
|
|
2042
|
+
)
|
|
2043
|
+
readme = tools_root / "README.md"
|
|
2044
|
+
if readme.exists():
|
|
2045
|
+
support_paths.append(readme)
|
|
2046
|
+
|
|
2047
|
+
# Some Skills embed their own client-side scripts directly under
|
|
2048
|
+
# skills/<id>/... (e.g. scripts/, references/) instead of tools/, and
|
|
2049
|
+
# their SKILL.md procedures instruct running them by relative path.
|
|
2050
|
+
# Without this, a remote client with no local checkout has no way to
|
|
2051
|
+
# obtain those scripts even though tools/ distribution is otherwise
|
|
2052
|
+
# unconditionally available once any Skill is selected.
|
|
2053
|
+
skills_root = root / "skills"
|
|
2054
|
+
if skills_root.exists():
|
|
2055
|
+
paths.extend(
|
|
2056
|
+
sorted(
|
|
2057
|
+
path
|
|
2058
|
+
for path in skills_root.glob("**/*.py")
|
|
2059
|
+
if "__pycache__" not in path.parts
|
|
2060
|
+
)
|
|
2061
|
+
)
|
|
2062
|
+
packs_root = root / "packs"
|
|
2063
|
+
if ownership is not None and packs_root.exists():
|
|
2064
|
+
paths.extend(
|
|
2065
|
+
sorted(
|
|
2066
|
+
path
|
|
2067
|
+
for path in packs_root.glob("*/skills/**/*.py")
|
|
2068
|
+
if "__pycache__" not in path.parts
|
|
2069
|
+
and ownership.distribution_enabled(relative_to_repo(path, root))
|
|
2070
|
+
)
|
|
2071
|
+
)
|
|
2072
|
+
paths.extend(
|
|
2073
|
+
sorted(
|
|
2074
|
+
path
|
|
2075
|
+
for path in packs_root.glob("local/*/skills/**/*.py")
|
|
2076
|
+
if "__pycache__" not in path.parts
|
|
2077
|
+
and ownership.distribution_enabled(relative_to_repo(path, root))
|
|
2078
|
+
)
|
|
2079
|
+
)
|
|
2080
|
+
|
|
2081
|
+
result: list[ClientToolFile] = []
|
|
2082
|
+
for path in [*paths, *support_paths]:
|
|
2083
|
+
rel = relative_to_repo(path, root)
|
|
2084
|
+
text = read_text(path)
|
|
2085
|
+
kind = _client_tool_kind(path)
|
|
2086
|
+
result.append(
|
|
2087
|
+
ClientToolFile(
|
|
2088
|
+
path=rel,
|
|
2089
|
+
kind=kind,
|
|
2090
|
+
content=text,
|
|
2091
|
+
content_hash=stable_hash(text),
|
|
2092
|
+
size_bytes=len(text.encode("utf-8")),
|
|
2093
|
+
run_hint=f"python {rel}" if kind == "python" else None,
|
|
2094
|
+
imports=_python_imports(text) if kind == "python" else [],
|
|
2095
|
+
links=markdown_xid_link_targets(text),
|
|
2096
|
+
)
|
|
2097
|
+
)
|
|
2098
|
+
return result
|
|
2099
|
+
|
|
2100
|
+
|
|
2101
|
+
def _client_tool_pip_package(root: Path) -> ClientToolPipPackage:
|
|
2102
|
+
# Scoped to tools/ only: these are the files declared installable via
|
|
2103
|
+
# [tool.setuptools.packages.find]. Skill-embedded scripts under skills/
|
|
2104
|
+
# are invoked by relative file path per their SKILL.md, not imported as
|
|
2105
|
+
# a package, and skills/ has no __init__.py chain making it one; bundling
|
|
2106
|
+
# them here would silently vanish on `pip install` since setuptools would
|
|
2107
|
+
# never install undiscovered loose files into site-packages. They remain
|
|
2108
|
+
# available via get_client_tool_file/get_client_tool_bundle instead.
|
|
2109
|
+
files = [file for file in _client_tool_files(root) if file.path.startswith("tools/")]
|
|
2110
|
+
package_root = f"xrefkit-client-tools-{CLIENT_TOOL_PACKAGE_VERSION}"
|
|
2111
|
+
buffer = io.BytesIO()
|
|
2112
|
+
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
2113
|
+
_zip_writestr(
|
|
2114
|
+
archive,
|
|
2115
|
+
f"{package_root}/pyproject.toml",
|
|
2116
|
+
_client_tools_pyproject(files),
|
|
2117
|
+
)
|
|
2118
|
+
_zip_writestr(
|
|
2119
|
+
archive,
|
|
2120
|
+
f"{package_root}/README.md",
|
|
2121
|
+
_client_tools_readme(),
|
|
2122
|
+
)
|
|
2123
|
+
_zip_writestr(
|
|
2124
|
+
archive,
|
|
2125
|
+
f"{package_root}/tools/__init__.py",
|
|
2126
|
+
'"""Client-side XRefKit deterministic tools."""\n',
|
|
2127
|
+
)
|
|
2128
|
+
for file in files:
|
|
2129
|
+
_zip_writestr(archive, f"{package_root}/{file.path}", file.content)
|
|
2130
|
+
content = buffer.getvalue()
|
|
2131
|
+
encoded = base64.b64encode(content).decode("ascii")
|
|
2132
|
+
return ClientToolPipPackage(
|
|
2133
|
+
filename=f"xrefkit-client-tools-{CLIENT_TOOL_PACKAGE_VERSION}.zip",
|
|
2134
|
+
package_id="xrefkit-client-tools",
|
|
2135
|
+
version=CLIENT_TOOL_PACKAGE_VERSION,
|
|
2136
|
+
package_format="zip-sdist",
|
|
2137
|
+
install_command=f"python -m pip install xrefkit-client-tools-{CLIENT_TOOL_PACKAGE_VERSION}.zip",
|
|
2138
|
+
content_base64=encoded,
|
|
2139
|
+
content_hash=hashlib_sha256_bytes(content),
|
|
2140
|
+
size_bytes=len(content),
|
|
2141
|
+
warnings=[
|
|
2142
|
+
"This package installs a top-level tools package to preserve existing XRefKit imports such as tools.error_policy_locator.",
|
|
2143
|
+
"Install in a project virtual environment to avoid conflicts with any unrelated package named tools.",
|
|
2144
|
+
"The package contains Python tools only; C# tools/structure_graph "
|
|
2145
|
+
"is not bundled. Install it as the NuGet dotnet tool "
|
|
2146
|
+
"XRefKit.StructureGraph (command dotnet-xrefkit-graph), build it "
|
|
2147
|
+
"from source, or receive precomputed graph JSON; see "
|
|
2148
|
+
"docs/guides/078_structure_graph_build_guide.md (resolve via "
|
|
2149
|
+
"get_document_by_xid with xid 8B3E5D0A94C7).",
|
|
2150
|
+
"The MCP server only distributes the package; tool execution is client-side.",
|
|
2151
|
+
"Skill-embedded scripts under skills/**/*.py are not included in this "
|
|
2152
|
+
"package. Fetch them with get_client_tool_file or "
|
|
2153
|
+
"get_client_tool_bundle and run them by their returned relative "
|
|
2154
|
+
"path instead.",
|
|
2155
|
+
],
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
def _client_tools_pyproject(files: list[ClientToolFile]) -> str:
|
|
2160
|
+
scripts: list[str] = []
|
|
2161
|
+
for file in files:
|
|
2162
|
+
if file.kind != "python" or "def main" not in file.content:
|
|
2163
|
+
continue
|
|
2164
|
+
module = file.path.removesuffix(".py").replace("/", ".")
|
|
2165
|
+
script_name = "xrefkit-" + Path(file.path).stem.replace("_", "-")
|
|
2166
|
+
scripts.append(f'{script_name} = "{module}:main"')
|
|
2167
|
+
scripts_block = "\n".join(sorted(scripts))
|
|
2168
|
+
return f"""[build-system]
|
|
2169
|
+
requires = ["setuptools>=68"]
|
|
2170
|
+
build-backend = "setuptools.build_meta"
|
|
2171
|
+
|
|
2172
|
+
[project]
|
|
2173
|
+
name = "xrefkit-client-tools"
|
|
2174
|
+
version = "{CLIENT_TOOL_PACKAGE_VERSION}"
|
|
2175
|
+
description = "Client-side deterministic Python tools distributed from XRefKit MCP"
|
|
2176
|
+
readme = "README.md"
|
|
2177
|
+
requires-python = ">=3.11"
|
|
2178
|
+
dependencies = []
|
|
2179
|
+
|
|
2180
|
+
[tool.setuptools.packages.find]
|
|
2181
|
+
include = ["tools*"]
|
|
2182
|
+
|
|
2183
|
+
[project.scripts]
|
|
2184
|
+
{scripts_block}
|
|
2185
|
+
"""
|
|
2186
|
+
|
|
2187
|
+
|
|
2188
|
+
def _client_tools_readme() -> str:
|
|
2189
|
+
return """# XRefKit Client Tools
|
|
2190
|
+
|
|
2191
|
+
This package is generated by XRefKit MCP and installs the Python files from
|
|
2192
|
+
`tools/` for client-side execution.
|
|
2193
|
+
|
|
2194
|
+
The MCP server does not execute these tools. Run them in the client-side target
|
|
2195
|
+
repository where the analyzed source files exist.
|
|
2196
|
+
|
|
2197
|
+
Examples:
|
|
2198
|
+
|
|
2199
|
+
```powershell
|
|
2200
|
+
python -m tools.cs_scope_probe --target .
|
|
2201
|
+
xrefkit-cs-scope-probe --target .
|
|
2202
|
+
```
|
|
2203
|
+
|
|
2204
|
+
Some tools require external programs such as git, dotnet, npm, or precomputed
|
|
2205
|
+
`tools/structure_graph` output.
|
|
2206
|
+
"""
|
|
2207
|
+
|
|
2208
|
+
|
|
2209
|
+
def _zip_writestr(archive: zipfile.ZipFile, name: str, content: str) -> None:
|
|
2210
|
+
# Fixed timestamp keeps rebuilt package bytes identical for identical
|
|
2211
|
+
# inputs, so a sha256 handed out earlier (for example in an MCP response
|
|
2212
|
+
# pointing at the HTTP /dist endpoint) still matches the artifact built
|
|
2213
|
+
# at download time.
|
|
2214
|
+
info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
|
|
2215
|
+
info.compress_type = zipfile.ZIP_DEFLATED
|
|
2216
|
+
info.external_attr = 0o644 << 16
|
|
2217
|
+
archive.writestr(info, content)
|
|
2218
|
+
|
|
2219
|
+
|
|
2220
|
+
def hashlib_sha256_bytes(content: bytes) -> str:
|
|
2221
|
+
import hashlib
|
|
2222
|
+
|
|
2223
|
+
return hashlib.sha256(content).hexdigest()
|
|
2224
|
+
|
|
2225
|
+
|
|
2226
|
+
def _client_tool_kind(path: Path) -> str:
|
|
2227
|
+
if path.suffix == ".py":
|
|
2228
|
+
return "python"
|
|
2229
|
+
if path.name.lower() == "readme.md":
|
|
2230
|
+
return "documentation"
|
|
2231
|
+
return "support"
|
|
2232
|
+
|
|
2233
|
+
|
|
2234
|
+
def _python_imports(text: str) -> list[str]:
|
|
2235
|
+
imports: list[str] = []
|
|
2236
|
+
for match in IMPORT_RE.finditer(text):
|
|
2237
|
+
module = match.group(1)
|
|
2238
|
+
if module not in imports:
|
|
2239
|
+
imports.append(module)
|
|
2240
|
+
return imports
|
|
2241
|
+
|
|
2242
|
+
|
|
2243
|
+
def _runtime_role_contract() -> RuntimeRoleContract:
|
|
2244
|
+
return RuntimeRoleContract(
|
|
2245
|
+
roles={
|
|
2246
|
+
"executor": "advances the execution phase and performs assigned work items",
|
|
2247
|
+
"checker": "advances deterministic check/progression verification and must differ from executor",
|
|
2248
|
+
"quality_reviewer": "advances quality acceptance for standard/heavy work and must differ from executor",
|
|
2249
|
+
"handoff_owner": "advances explicit handoff phase and unresolved-item transfer",
|
|
2250
|
+
},
|
|
2251
|
+
phases=["startup", "planning", "execution", "check", "quality", "closure", "handoff"],
|
|
2252
|
+
statuses=["pending", "in_progress", "done", "blocked", "unknown", "escalated"],
|
|
2253
|
+
invariants=[
|
|
2254
|
+
"Skill execution starts through xrefkit skill run before opening SKILL.md",
|
|
2255
|
+
"execution/check/quality roles are separated from the executor role",
|
|
2256
|
+
"check is deterministic progression verification via xrefkit skill verify",
|
|
2257
|
+
"quality is a separate acceptance axis for standard/heavy work",
|
|
2258
|
+
"unknowns must resolve before closure; risks must resolve or escalate",
|
|
2259
|
+
"closure requires work items plus output and evidence artifacts",
|
|
2260
|
+
"workflow steps transition through gates, not through bare model judgment",
|
|
2261
|
+
],
|
|
2262
|
+
required_commands=[
|
|
2263
|
+
"python -m xrefkit skill run --meta <path-to-meta.md> --task \"<task>\" --json",
|
|
2264
|
+
"python -m xrefkit skill workitem --log <run-log> --item <id> --status <status> --role <assigned-role>",
|
|
2265
|
+
"python -m xrefkit skill artifact --log <run-log> --artifact <id> --kind <kind> --target <target> --status <status> --role <assigned-role>",
|
|
2266
|
+
"python -m xrefkit skill concern --log <run-log> --concern <id> --kind <unknown|risk|judgment> --status <status> --role <assigned-role>",
|
|
2267
|
+
"python -m xrefkit skill verify --log <run-log>",
|
|
2268
|
+
"python -m xrefkit skill close --log <run-log>",
|
|
2269
|
+
],
|
|
2270
|
+
source_xids=["B7A2C94F0E61", "4C7E9A2B1D63"],
|
|
2271
|
+
)
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
def _missing_skill_fields(meta: dict[str, object], has_skill_doc: bool) -> list[str]:
|
|
2275
|
+
required = [
|
|
2276
|
+
"skill_id",
|
|
2277
|
+
"summary",
|
|
2278
|
+
"maturity",
|
|
2279
|
+
"input",
|
|
2280
|
+
"output",
|
|
2281
|
+
]
|
|
2282
|
+
missing = [field for field in required if not meta.get(field)]
|
|
2283
|
+
for field in ["intent", "target_artifacts", "applies_when", "not_for", "required_tools"]:
|
|
2284
|
+
if not meta.get(field):
|
|
2285
|
+
missing.append(field)
|
|
2286
|
+
if not has_skill_doc:
|
|
2287
|
+
missing.append("skill_doc")
|
|
2288
|
+
return missing
|
|
2289
|
+
|
|
2290
|
+
|
|
2291
|
+
def _derive_intent(meta: dict[str, object]) -> list[str]:
|
|
2292
|
+
explicit = scalar_list(meta, "intent")
|
|
2293
|
+
if explicit:
|
|
2294
|
+
return explicit
|
|
2295
|
+
tags = scalar_list(meta, "tags")
|
|
2296
|
+
use_when = str(meta.get("use_when") or "")
|
|
2297
|
+
values = [tag for tag in tags if tag in {"review", "design", "routing", "quality"}]
|
|
2298
|
+
if "review" in use_when.lower() and "review" not in values:
|
|
2299
|
+
values.append("review")
|
|
2300
|
+
if "route" in use_when.lower() and "routing" not in values:
|
|
2301
|
+
values.append("routing")
|
|
2302
|
+
return values
|
|
2303
|
+
|
|
2304
|
+
|
|
2305
|
+
def _derive_target_artifacts(meta: dict[str, object]) -> list[str]:
|
|
2306
|
+
explicit = scalar_list(meta, "target_artifacts")
|
|
2307
|
+
if explicit:
|
|
2308
|
+
return explicit
|
|
2309
|
+
haystack = " ".join(
|
|
2310
|
+
[str(meta.get("use_when") or ""), str(meta.get("input") or ""), *scalar_list(meta, "tags")]
|
|
2311
|
+
).lower()
|
|
2312
|
+
targets: list[str] = []
|
|
2313
|
+
for needle, target in [
|
|
2314
|
+
("c#", "csharp_source"),
|
|
2315
|
+
("dotnet", "dotnet_source"),
|
|
2316
|
+
("ddl", "ddl"),
|
|
2317
|
+
("api", "api_contract"),
|
|
2318
|
+
("screen", "ui_spec"),
|
|
2319
|
+
("design", "design_artifact"),
|
|
2320
|
+
("code", "source_code"),
|
|
2321
|
+
]:
|
|
2322
|
+
if needle in haystack and target not in targets:
|
|
2323
|
+
targets.append(target)
|
|
2324
|
+
return targets
|
|
2325
|
+
|
|
2326
|
+
|
|
2327
|
+
def _split_constraints(value: str) -> list[str]:
|
|
2328
|
+
if not value:
|
|
2329
|
+
return []
|
|
2330
|
+
pieces = re.split(r";|,|—|--", value)
|
|
2331
|
+
return [piece.strip() for piece in pieces if piece.strip()]
|
|
2332
|
+
|
|
2333
|
+
|
|
2334
|
+
def _knowledge_req(ref: str) -> dict[str, object]:
|
|
2335
|
+
xid_match = re.search(r"#xid-([A-Za-z0-9]+)", ref)
|
|
2336
|
+
return {
|
|
2337
|
+
"xid": xid_match.group(1) if xid_match else ref,
|
|
2338
|
+
"version": 1,
|
|
2339
|
+
"required_when": "declared by Skill meta knowledge_refs",
|
|
2340
|
+
"detail_policy": "expand_on_demand",
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
def _bind_knowledge_req(slot: dict) -> dict[str, object]:
|
|
2345
|
+
# A pinned (bind) knowledge_slot is a required-knowledge XID; query slots are
|
|
2346
|
+
# resolved dynamically via resolve_skill_knowledge instead.
|
|
2347
|
+
return {
|
|
2348
|
+
"xid": str(slot.get("bind")),
|
|
2349
|
+
"version": 1,
|
|
2350
|
+
"required_when": "declared by Skill meta knowledge_slot bind",
|
|
2351
|
+
"detail_policy": "expand_on_demand",
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
|
|
2355
|
+
def _required_tool(name: str) -> dict[str, object]:
|
|
2356
|
+
if name.startswith("xref."):
|
|
2357
|
+
return {
|
|
2358
|
+
"tool_id": name,
|
|
2359
|
+
"required_when": "declared by Skill meta required_tools",
|
|
2360
|
+
}
|
|
2361
|
+
return {
|
|
2362
|
+
"name": name,
|
|
2363
|
+
"execution_location": "client",
|
|
2364
|
+
"required_when": "declared by Skill meta required_tools",
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
|
|
2368
|
+
def _section_bullets(text: str, heading: str) -> list[str]:
|
|
2369
|
+
lines = text.splitlines()
|
|
2370
|
+
in_section = False
|
|
2371
|
+
result: list[str] = []
|
|
2372
|
+
for line in lines:
|
|
2373
|
+
if line.startswith("## "):
|
|
2374
|
+
in_section = line.strip("# ").strip().lower() == heading.lower()
|
|
2375
|
+
continue
|
|
2376
|
+
if in_section and line.startswith("- "):
|
|
2377
|
+
result.append(line[2:].strip())
|
|
2378
|
+
return result
|
|
2379
|
+
|
|
2380
|
+
|
|
2381
|
+
def _nested_value(meta: dict[str, object], _parent: str, _key: str) -> str | None:
|
|
2382
|
+
# Current XRefKit meta files use prose-like nested bullets. They are kept
|
|
2383
|
+
# opaque by the lightweight parser, so return None until a structured field
|
|
2384
|
+
# exists in the source repository.
|
|
2385
|
+
return None
|
|
2386
|
+
|
|
2387
|
+
|
|
2388
|
+
def _yaml_top_scalars(text: str) -> dict[str, str]:
|
|
2389
|
+
result: dict[str, str] = {}
|
|
2390
|
+
for raw in text.splitlines():
|
|
2391
|
+
if raw.startswith(" ") or not raw.strip() or raw.lstrip().startswith("#"):
|
|
2392
|
+
continue
|
|
2393
|
+
match = re.match(r"^([A-Za-z0-9_]+):\s*(.+?)\s*$", raw)
|
|
2394
|
+
if match and not match.group(2).startswith("["):
|
|
2395
|
+
result[match.group(1)] = match.group(2).strip().strip('"')
|
|
2396
|
+
return result
|
|
2397
|
+
|
|
2398
|
+
|
|
2399
|
+
def _yaml_nested_scalar(text: str, parent: str, key: str) -> str | None:
|
|
2400
|
+
in_parent = False
|
|
2401
|
+
for raw in text.splitlines():
|
|
2402
|
+
if re.match(rf"^{re.escape(parent)}:\s*$", raw):
|
|
2403
|
+
in_parent = True
|
|
2404
|
+
continue
|
|
2405
|
+
if in_parent and raw and not raw.startswith(" "):
|
|
2406
|
+
return None
|
|
2407
|
+
if in_parent:
|
|
2408
|
+
match = re.match(rf"^\s+{re.escape(key)}:\s*(.+?)\s*$", raw)
|
|
2409
|
+
if match:
|
|
2410
|
+
return match.group(1).strip().strip('"')
|
|
2411
|
+
return None
|
|
2412
|
+
|
|
2413
|
+
|
|
2414
|
+
def _yaml_top_list(text: str, key: str) -> list[str]:
|
|
2415
|
+
in_list = False
|
|
2416
|
+
result: list[str] = []
|
|
2417
|
+
for raw in text.splitlines():
|
|
2418
|
+
if re.match(rf"^{re.escape(key)}:\s*$", raw):
|
|
2419
|
+
in_list = True
|
|
2420
|
+
continue
|
|
2421
|
+
if in_list and raw and not raw.startswith(" "):
|
|
2422
|
+
break
|
|
2423
|
+
if in_list:
|
|
2424
|
+
match = re.match(r"^\s+-\s+(.+?)\s*$", raw)
|
|
2425
|
+
if match:
|
|
2426
|
+
result.append(match.group(1).strip().strip('"'))
|
|
2427
|
+
return result
|
|
2428
|
+
|
|
2429
|
+
|
|
2430
|
+
def _yaml_map_keys(text: str, key: str) -> list[str]:
|
|
2431
|
+
in_map = False
|
|
2432
|
+
result: list[str] = []
|
|
2433
|
+
for raw in text.splitlines():
|
|
2434
|
+
if re.match(rf"^{re.escape(key)}:\s*$", raw):
|
|
2435
|
+
in_map = True
|
|
2436
|
+
continue
|
|
2437
|
+
if in_map and raw and not raw.startswith(" "):
|
|
2438
|
+
break
|
|
2439
|
+
if in_map:
|
|
2440
|
+
match = re.match(r"^\s{2}([A-Za-z0-9_]+):\s*$", raw)
|
|
2441
|
+
if match:
|
|
2442
|
+
result.append(match.group(1))
|
|
2443
|
+
return result
|
|
2444
|
+
|
|
2445
|
+
|
|
2446
|
+
def _yaml_values_for_key(text: str, key: str) -> list[str]:
|
|
2447
|
+
values: list[str] = []
|
|
2448
|
+
for raw in text.splitlines():
|
|
2449
|
+
match = re.match(rf"^\s*{re.escape(key)}:\s*(.+?)\s*$", raw)
|
|
2450
|
+
if match:
|
|
2451
|
+
values.append(match.group(1).strip().strip('"'))
|
|
2452
|
+
return list(dict.fromkeys(values))
|
|
2453
|
+
|
|
2454
|
+
|
|
2455
|
+
def _rank_entries(query: str, entries: list[KnowledgeCatalogEntry]) -> list[KnowledgeCatalogEntry]:
|
|
2456
|
+
query_tokens = _tokens(query)
|
|
2457
|
+
return sorted(
|
|
2458
|
+
entries,
|
|
2459
|
+
key=lambda entry: (
|
|
2460
|
+
len(query_tokens & _tokens(" ".join([entry.title, entry.summary, entry.domain]))),
|
|
2461
|
+
entry.title,
|
|
2462
|
+
),
|
|
2463
|
+
reverse=True,
|
|
2464
|
+
)
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
def _tokens(value: str) -> set[str]:
|
|
2468
|
+
normalized = (
|
|
2469
|
+
value.lower()
|
|
2470
|
+
.replace("_", " ")
|
|
2471
|
+
.replace("-", " ")
|
|
2472
|
+
.replace("c#", "csharp")
|
|
2473
|
+
.replace(".net", "dotnet")
|
|
2474
|
+
.replace("non-roslyn", "roslyn")
|
|
2475
|
+
)
|
|
2476
|
+
tokens = {match.group(0).lower() for match in TOKEN_RE.finditer(normalized)}
|
|
2477
|
+
for alias, expanded in ROUTING_SYNONYMS.items():
|
|
2478
|
+
if alias.lower() in normalized:
|
|
2479
|
+
tokens.update(expanded)
|
|
2480
|
+
if "roslyn" in tokens:
|
|
2481
|
+
tokens.add("diagnostics")
|
|
2482
|
+
if "csharp" in tokens:
|
|
2483
|
+
tokens.add("c#")
|
|
2484
|
+
if "script" in tokens:
|
|
2485
|
+
tokens.add("automation")
|
|
2486
|
+
if "tool" in tokens:
|
|
2487
|
+
tokens.add("tooling")
|
|
2488
|
+
if "traceability" in tokens:
|
|
2489
|
+
tokens.add("trace")
|
|
2490
|
+
return tokens
|
|
2491
|
+
|
|
2492
|
+
|
|
2493
|
+
def _knowledge_slot_text(slot: dict) -> str:
|
|
2494
|
+
return " ".join(str(value) for value in slot.values() if value is not None)
|
|
2495
|
+
|
|
2496
|
+
|
|
2497
|
+
def _routing_categories(tokens: set[str]) -> dict[str, set[str]]:
|
|
2498
|
+
effective_tokens = tokens - STOP_TOKENS
|
|
2499
|
+
categories: dict[str, set[str]] = {}
|
|
2500
|
+
for category, terms in ROUTING_CATEGORY_TERMS.items():
|
|
2501
|
+
matched = effective_tokens & terms
|
|
2502
|
+
if matched:
|
|
2503
|
+
categories[category] = matched
|
|
2504
|
+
return categories
|
|
2505
|
+
|
|
2506
|
+
|
|
2507
|
+
def _skill_values_by_category(skill: SkillCatalogEntry) -> dict[str, list[str]]:
|
|
2508
|
+
closure_values = [
|
|
2509
|
+
*skill.closure_contract.closure_conditions,
|
|
2510
|
+
skill.closure_contract.handoff_policy,
|
|
2511
|
+
skill.closure_contract.worklist_policy,
|
|
2512
|
+
]
|
|
2513
|
+
knowledge_slot_values = [_knowledge_slot_text(slot) for slot in skill.knowledge_slots]
|
|
2514
|
+
tool_values = [
|
|
2515
|
+
" ".join(str(value) for value in tool.values() if value is not None)
|
|
2516
|
+
for tool in skill.required_tools
|
|
2517
|
+
]
|
|
2518
|
+
return {
|
|
2519
|
+
"activity": [
|
|
2520
|
+
skill.skill_id,
|
|
2521
|
+
skill.summary,
|
|
2522
|
+
skill.capability,
|
|
2523
|
+
skill.tuning,
|
|
2524
|
+
skill.responsibility,
|
|
2525
|
+
*skill.intent,
|
|
2526
|
+
*skill.applies_when,
|
|
2527
|
+
],
|
|
2528
|
+
"artifact": [
|
|
2529
|
+
skill.skill_id,
|
|
2530
|
+
skill.summary,
|
|
2531
|
+
*skill.target_artifacts,
|
|
2532
|
+
*skill.inputs,
|
|
2533
|
+
*skill.outputs,
|
|
2534
|
+
],
|
|
2535
|
+
"domain": [
|
|
2536
|
+
skill.skill_id,
|
|
2537
|
+
skill.summary,
|
|
2538
|
+
skill.tuning,
|
|
2539
|
+
skill.responsibility,
|
|
2540
|
+
*skill.inputs,
|
|
2541
|
+
*skill.outputs,
|
|
2542
|
+
*knowledge_slot_values,
|
|
2543
|
+
],
|
|
2544
|
+
"phase": [
|
|
2545
|
+
skill.summary,
|
|
2546
|
+
*skill.applies_when,
|
|
2547
|
+
*skill.inputs,
|
|
2548
|
+
*skill.outputs,
|
|
2549
|
+
*closure_values,
|
|
2550
|
+
],
|
|
2551
|
+
"evidence_trace": [
|
|
2552
|
+
skill.summary,
|
|
2553
|
+
*skill.inputs,
|
|
2554
|
+
*skill.outputs,
|
|
2555
|
+
*closure_values,
|
|
2556
|
+
*knowledge_slot_values,
|
|
2557
|
+
],
|
|
2558
|
+
"tool_runtime": [
|
|
2559
|
+
skill.skill_id,
|
|
2560
|
+
skill.summary,
|
|
2561
|
+
*skill.inputs,
|
|
2562
|
+
*skill.outputs,
|
|
2563
|
+
*tool_values,
|
|
2564
|
+
*knowledge_slot_values,
|
|
2565
|
+
],
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
|
|
2569
|
+
def _mcp_content_resolution_policy() -> dict[str, str]:
|
|
2570
|
+
return {
|
|
2571
|
+
"mode": "mcp_only",
|
|
2572
|
+
"xid_document_tool": "get_document_by_xid",
|
|
2573
|
+
"skill_body_tool": "get_skill",
|
|
2574
|
+
"path_like_fields": "server_side_identity_or_diagnostic_only",
|
|
2575
|
+
"client_filesystem_resolution": "forbidden",
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
|
|
2579
|
+
def _matched_routing_categories(
|
|
2580
|
+
skill: SkillCatalogEntry, query_categories: dict[str, set[str]]
|
|
2581
|
+
) -> dict[str, list[str]]:
|
|
2582
|
+
values_by_category = _skill_values_by_category(skill)
|
|
2583
|
+
matched_categories: dict[str, list[str]] = {}
|
|
2584
|
+
for category, query_terms in query_categories.items():
|
|
2585
|
+
values = values_by_category.get(category, [])
|
|
2586
|
+
matches: list[str] = []
|
|
2587
|
+
for value in values:
|
|
2588
|
+
value_tokens = _tokens(value) - STOP_TOKENS
|
|
2589
|
+
matched_terms = sorted(query_terms & value_tokens)
|
|
2590
|
+
if matched_terms:
|
|
2591
|
+
matches.append(f"{value} [{', '.join(matched_terms)}]")
|
|
2592
|
+
if matches:
|
|
2593
|
+
matched_categories[category] = matches[:5]
|
|
2594
|
+
return matched_categories
|
|
2595
|
+
|
|
2596
|
+
|
|
2597
|
+
def _has_required_category_coverage(
|
|
2598
|
+
query_categories: dict[str, set[str]], matched_categories: dict[str, list[str]]
|
|
2599
|
+
) -> bool:
|
|
2600
|
+
for category in ["domain", "tool_runtime", "evidence_trace"]:
|
|
2601
|
+
if category in query_categories and category not in matched_categories:
|
|
2602
|
+
return False
|
|
2603
|
+
return True
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
def _category_coverage_multiplier(
|
|
2607
|
+
query_categories: dict[str, set[str]], matched_categories: dict[str, list[str]]
|
|
2608
|
+
) -> float:
|
|
2609
|
+
multiplier = 1.0
|
|
2610
|
+
for category, penalty in [
|
|
2611
|
+
("domain", 0.55),
|
|
2612
|
+
("tool_runtime", 0.45),
|
|
2613
|
+
("evidence_trace", 0.7),
|
|
2614
|
+
]:
|
|
2615
|
+
if category in query_categories and category not in matched_categories:
|
|
2616
|
+
multiplier *= penalty
|
|
2617
|
+
return multiplier
|
|
2618
|
+
|
|
2619
|
+
|
|
2620
|
+
def _matched_values(
|
|
2621
|
+
query_tokens: set[str], values: list[str], use_stop_words: bool = False
|
|
2622
|
+
) -> list[str]:
|
|
2623
|
+
matched: list[str] = []
|
|
2624
|
+
for value in values:
|
|
2625
|
+
value_tokens = _tokens(value)
|
|
2626
|
+
if use_stop_words:
|
|
2627
|
+
value_tokens = value_tokens - STOP_TOKENS
|
|
2628
|
+
effective_query = query_tokens - STOP_TOKENS
|
|
2629
|
+
else:
|
|
2630
|
+
effective_query = query_tokens
|
|
2631
|
+
if effective_query & value_tokens:
|
|
2632
|
+
matched.append(value)
|
|
2633
|
+
return matched
|
|
2634
|
+
|
|
2635
|
+
|
|
2636
|
+
def _overlap_count(query_tokens: set[str], values: list[str]) -> int:
|
|
2637
|
+
value_tokens = set().union(*(_tokens(value) for value in values)) if values else set()
|
|
2638
|
+
return len((query_tokens - STOP_TOKENS) & (value_tokens - STOP_TOKENS))
|