xrefkit 0.3.0__py3-none-any.whl

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