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/cli.py ADDED
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from .catalog import XRefCatalog
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> int:
11
+ parser = argparse.ArgumentParser(prog="xrefkit-mcp-catalog")
12
+ sub = parser.add_subparsers(dest="command", required=True)
13
+
14
+ def add_repo_arguments(command: argparse.ArgumentParser) -> None:
15
+ command.add_argument("--repo", required=True)
16
+ command.add_argument(
17
+ "--domain-knowledge-root",
18
+ action="append",
19
+ default=[],
20
+ help="External XID-addressable domain knowledge root. Can be repeated.",
21
+ )
22
+
23
+ catalog = sub.add_parser("catalog", help="build and print catalog summary")
24
+ add_repo_arguments(catalog)
25
+
26
+ identity = sub.add_parser(
27
+ "repository-identity",
28
+ help="print the repository cache identity",
29
+ )
30
+ add_repo_arguments(identity)
31
+
32
+ startup = sub.add_parser("startup-context", help="print required startup references")
33
+ add_repo_arguments(startup)
34
+
35
+ pack_hashes = sub.add_parser(
36
+ "startup-pack-hashes",
37
+ help="print the Based On hash lines for the startup contract pack document",
38
+ )
39
+ add_repo_arguments(pack_hashes)
40
+
41
+ pack_check = sub.add_parser(
42
+ "check-startup-pack",
43
+ help="exit non-zero when the startup contract pack is stale against its sources",
44
+ )
45
+ add_repo_arguments(pack_check)
46
+
47
+ knowledge = sub.add_parser("search-knowledge", help="search knowledge catalog")
48
+ add_repo_arguments(knowledge)
49
+ knowledge.add_argument("--query", required=True)
50
+ knowledge.add_argument("--limit", type=int, default=10)
51
+
52
+ expand = sub.add_parser("expand-knowledge", help="expand one knowledge body")
53
+ add_repo_arguments(expand)
54
+ expand.add_argument("--xid", required=True)
55
+
56
+ document = sub.add_parser("get-document", help="expand any managed Markdown document by XID")
57
+ add_repo_arguments(document)
58
+ document.add_argument("--xid", required=True)
59
+ document.add_argument("--known-version")
60
+
61
+ context = sub.add_parser("build-knowledge-context", help="expand bounded knowledge context")
62
+ add_repo_arguments(context)
63
+ context.add_argument("--query", required=True)
64
+ context.add_argument("--limit", type=int, default=5)
65
+
66
+ skills = sub.add_parser("list-skills", help="list Skill catalog")
67
+ add_repo_arguments(skills)
68
+ skills.add_argument("--limit", type=int)
69
+ skills.add_argument(
70
+ "--include-content",
71
+ action="store_true",
72
+ help="Include full meta.md and SKILL.md bodies (metadata-only by default)",
73
+ )
74
+
75
+ skill = sub.add_parser("get-skill", help="get one Skill catalog entry with transferred content")
76
+ add_repo_arguments(skill)
77
+ skill.add_argument("--skill-id", required=True)
78
+
79
+ rank = sub.add_parser("rank-skills", help="rank Skill candidates for a purpose")
80
+ add_repo_arguments(rank)
81
+ rank.add_argument("--purpose", required=True)
82
+ rank.add_argument("--limit", type=int, default=5)
83
+
84
+ contracts = sub.add_parser("tool-contracts", help="list read-only tool contracts")
85
+ add_repo_arguments(contracts)
86
+
87
+ tool_manifest = sub.add_parser("client-tool-manifest", help="list distributable client-side tool files")
88
+ add_repo_arguments(tool_manifest)
89
+
90
+ tool_file = sub.add_parser("get-client-tool-file", help="get one distributable client-side tool file")
91
+ add_repo_arguments(tool_file)
92
+ tool_file.add_argument("--path", required=True)
93
+
94
+ tool_bundle = sub.add_parser("client-tool-bundle", help="get all distributable client-side tool files")
95
+ add_repo_arguments(tool_bundle)
96
+
97
+ tool_package = sub.add_parser("client-tool-pip-package", help="get a pip-installable client tool package")
98
+ add_repo_arguments(tool_package)
99
+
100
+ version_check = sub.add_parser("check-client-tool-versions", help="check installed client tool versions")
101
+ add_repo_arguments(version_check)
102
+ version_check.add_argument(
103
+ "--installed",
104
+ action="append",
105
+ default=[],
106
+ help="Installed package version as package_id=version. Can be repeated.",
107
+ )
108
+
109
+ args = parser.parse_args(argv)
110
+ model = XRefCatalog.build(Path(args.repo), args.domain_knowledge_root)
111
+
112
+ if args.command == "catalog":
113
+ payload = {
114
+ "catalog_version": model.catalog_version,
115
+ "knowledge_count": len(model.knowledge),
116
+ "skill_count": len(model.skills),
117
+ "tool_contract_count": len(model.tools),
118
+ }
119
+ elif args.command == "repository-identity":
120
+ payload = model.get_repository_identity()
121
+ elif args.command == "startup-context":
122
+ payload = model.get_startup_context()
123
+ elif args.command == "startup-pack-hashes":
124
+ references = model.get_startup_context()["references"]
125
+ for reference in references:
126
+ print(f"- {reference['xid']}: `{reference['content_hash']}`")
127
+ return 0
128
+ elif args.command == "check-startup-pack":
129
+ pack = model.get_startup_context()["startup_contract_pack"]
130
+ payload = {
131
+ "pack_source": pack["pack_source"],
132
+ "pack_doc_xid": pack["pack_doc_xid"],
133
+ "stale": pack["stale"],
134
+ "stale_sources": pack["stale_sources"],
135
+ }
136
+ print(json.dumps(payload, ensure_ascii=True, indent=2))
137
+ return 1 if pack["stale"] else 0
138
+ elif args.command == "search-knowledge":
139
+ payload = model.search_knowledge_catalog(args.query, args.limit)
140
+ elif args.command == "expand-knowledge":
141
+ payload = model.expand_knowledge(args.xid)
142
+ elif args.command == "get-document":
143
+ payload = model.get_document_by_xid(args.xid, args.known_version)
144
+ elif args.command == "build-knowledge-context":
145
+ payload = model.build_knowledge_context(args.query, args.limit)
146
+ elif args.command == "list-skills":
147
+ payload = model.list_skills(args.limit, args.include_content)
148
+ elif args.command == "get-skill":
149
+ payload = model.get_skill(args.skill_id)
150
+ elif args.command == "rank-skills":
151
+ payload = model.rank_skills_for_purpose(args.purpose, args.limit)
152
+ elif args.command == "tool-contracts":
153
+ payload = model.list_tool_contracts()
154
+ elif args.command == "client-tool-manifest":
155
+ payload = model.get_client_tool_manifest()
156
+ elif args.command == "get-client-tool-file":
157
+ payload = model.get_client_tool_file(args.path)
158
+ elif args.command == "client-tool-bundle":
159
+ payload = model.get_client_tool_bundle()
160
+ elif args.command == "client-tool-pip-package":
161
+ payload = model.get_client_tool_pip_package()
162
+ elif args.command == "check-client-tool-versions":
163
+ installed = dict(item.split("=", 1) for item in args.installed)
164
+ payload = model.check_client_tool_versions(installed)
165
+ else:
166
+ parser.error(f"unknown command: {args.command}")
167
+
168
+ print(json.dumps(payload, ensure_ascii=True, indent=2))
169
+ return 0
170
+
171
+
172
+ if __name__ == "__main__":
173
+ raise SystemExit(main())
@@ -0,0 +1,356 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import tempfile
6
+ from collections.abc import Awaitable, Callable, Iterable
7
+ from datetime import UTC, datetime
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .repository import stable_hash
12
+
13
+
14
+ XID_PATTERN = re.compile(r"^[A-Za-z0-9]+$")
15
+ NAMESPACE_PATTERN = re.compile(r"^[A-Za-z0-9._-]+$")
16
+
17
+
18
+ class DocumentCacheProtocolError(RuntimeError):
19
+ pass
20
+
21
+
22
+ class XidDocumentCache:
23
+ def __init__(
24
+ self,
25
+ cache_root: str | Path,
26
+ repository_fingerprint: str,
27
+ ) -> None:
28
+ if not NAMESPACE_PATTERN.fullmatch(repository_fingerprint):
29
+ raise ValueError(
30
+ f"invalid repository fingerprint: {repository_fingerprint!r}"
31
+ )
32
+ self.cache_root = Path(cache_root)
33
+ self.repository_fingerprint = repository_fingerprint
34
+ self.cache_dir = self.cache_root / repository_fingerprint
35
+
36
+ def load(self, xid: str) -> dict[str, Any] | None:
37
+ path = self._entry_path(xid)
38
+ try:
39
+ payload = json.loads(path.read_text(encoding="utf-8"))
40
+ document = payload["document"]
41
+ content_hash = payload["content_hash"]
42
+ if payload["repository_fingerprint"] != self.repository_fingerprint:
43
+ raise ValueError("cached repository fingerprint does not match")
44
+ if document["xid"] != xid:
45
+ raise ValueError("cached XID does not match the cache key")
46
+ if (
47
+ document["repository_fingerprint"]
48
+ != self.repository_fingerprint
49
+ ):
50
+ raise ValueError("document repository fingerprint does not match")
51
+ if document["content_hash"] != content_hash:
52
+ raise ValueError("cached content_hash does not match document")
53
+ if stable_hash(document["content"]) != content_hash:
54
+ raise ValueError("cached content hash is invalid")
55
+ return document
56
+ except FileNotFoundError:
57
+ return None
58
+ except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
59
+ self.evict(xid)
60
+ return None
61
+
62
+ def known_versions(
63
+ self,
64
+ xids: Iterable[str] | None = None,
65
+ ) -> dict[str, str]:
66
+ if xids is not None:
67
+ versions: dict[str, str] = {}
68
+ for xid in xids:
69
+ document = self.load(xid)
70
+ if document is not None:
71
+ versions[xid] = document["content_hash"]
72
+ return versions
73
+ if not self.cache_dir.exists():
74
+ return {}
75
+ versions: dict[str, str] = {}
76
+ for path in sorted(self.cache_dir.glob("*.json")):
77
+ xid = path.stem
78
+ if not XID_PATTERN.fullmatch(xid):
79
+ continue
80
+ document = self.load(xid)
81
+ if document is not None:
82
+ versions[xid] = document["content_hash"]
83
+ return versions
84
+
85
+ def store(self, document: dict[str, Any]) -> bool:
86
+ xid = str(document.get("xid", ""))
87
+ self._validate_xid(xid)
88
+ if (
89
+ document.get("repository_fingerprint")
90
+ != self.repository_fingerprint
91
+ ):
92
+ raise DocumentCacheProtocolError(
93
+ "document repository fingerprint does not match cache namespace"
94
+ )
95
+ policy = document.get("cache_policy") or {}
96
+ if policy.get("cache_recommended") is not True:
97
+ self.evict(xid)
98
+ return False
99
+ content = document.get("content")
100
+ version = document.get("content_hash")
101
+ if not isinstance(content, str) or not isinstance(version, str):
102
+ raise DocumentCacheProtocolError(
103
+ "cacheable document must include content and content_hash"
104
+ )
105
+ if stable_hash(content) != version:
106
+ raise DocumentCacheProtocolError(
107
+ "document content does not match content_hash"
108
+ )
109
+
110
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
111
+ target = self._entry_path(xid)
112
+ payload = {
113
+ "schema_version": 1,
114
+ "repository_fingerprint": self.repository_fingerprint,
115
+ "xid": xid,
116
+ "content_hash": version,
117
+ "document": document,
118
+ }
119
+ serialized = json.dumps(
120
+ payload,
121
+ ensure_ascii=False,
122
+ indent=2,
123
+ sort_keys=True,
124
+ )
125
+ self._write_atomic(target, serialized)
126
+ return True
127
+
128
+ def startup_versions(self) -> dict[str, str]:
129
+ try:
130
+ payload = json.loads(
131
+ self._startup_index_path().read_text(encoding="utf-8")
132
+ )
133
+ if payload["repository_fingerprint"] != self.repository_fingerprint:
134
+ raise ValueError("startup repository fingerprint does not match")
135
+ xids = payload["xids"]
136
+ if not isinstance(xids, list) or not all(
137
+ isinstance(xid, str) for xid in xids
138
+ ):
139
+ raise ValueError("invalid startup XID index")
140
+ except (
141
+ FileNotFoundError,
142
+ OSError,
143
+ KeyError,
144
+ TypeError,
145
+ ValueError,
146
+ json.JSONDecodeError,
147
+ ):
148
+ return {}
149
+ return self.known_versions(xids)
150
+
151
+ async def resolve_startup(
152
+ self,
153
+ fetch_startup: Callable[
154
+ [dict[str, str]],
155
+ Awaitable[dict[str, Any]],
156
+ ],
157
+ ) -> dict[str, Any]:
158
+ known_versions = self.startup_versions()
159
+ response = await fetch_startup(known_versions)
160
+ try:
161
+ references = [
162
+ self.materialize(reference)
163
+ for reference in response["references"]
164
+ ]
165
+ except (DocumentCacheProtocolError, KeyError, TypeError):
166
+ known_versions = {}
167
+ response = await fetch_startup(known_versions)
168
+ references = [
169
+ self.materialize(reference)
170
+ for reference in response["references"]
171
+ ]
172
+ result = dict(response)
173
+ result["references"] = references
174
+ self._append_startup_decision_log(response, references, known_versions)
175
+ self._store_startup_index(
176
+ [str(reference["xid"]) for reference in references]
177
+ )
178
+ return result
179
+
180
+ def startup_decision_log_path(self) -> Path:
181
+ return self.cache_dir / "_startup_decisions.jsonl"
182
+
183
+ def startup_decision_log_entries(self) -> list[dict[str, Any]]:
184
+ try:
185
+ lines = self.startup_decision_log_path().read_text(
186
+ encoding="utf-8"
187
+ ).splitlines()
188
+ except FileNotFoundError:
189
+ return []
190
+ entries: list[dict[str, Any]] = []
191
+ for line in lines:
192
+ if not line.strip():
193
+ continue
194
+ entries.append(json.loads(line))
195
+ return entries
196
+
197
+ def _append_startup_decision_log(
198
+ self,
199
+ response: dict[str, Any],
200
+ references: list[dict[str, Any]],
201
+ known_versions: dict[str, str],
202
+ ) -> None:
203
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
204
+ startup_contract_pack = response.get("startup_contract_pack")
205
+ source_xids = _string_list(
206
+ startup_contract_pack.get("source_xids")
207
+ if isinstance(startup_contract_pack, dict)
208
+ else None
209
+ )
210
+ load_order_xids = _string_list(response.get("load_order"))
211
+ entry = {
212
+ "schema_version": 1,
213
+ "event": "startup_xid_decision",
214
+ "logged_at": datetime.now(UTC).isoformat(),
215
+ "repository_fingerprint": self.repository_fingerprint,
216
+ "known_version_xids": sorted(known_versions),
217
+ "load_order_xids": load_order_xids,
218
+ "startup_contract_pack_source_xids": source_xids,
219
+ "reference_xids": [str(reference["xid"]) for reference in references],
220
+ "client_decision_xids": _unique_strings(
221
+ [
222
+ *load_order_xids,
223
+ *source_xids,
224
+ *(str(reference["xid"]) for reference in references),
225
+ ]
226
+ ),
227
+ }
228
+ with self.startup_decision_log_path().open(
229
+ mode="a",
230
+ encoding="utf-8",
231
+ ) as log_file:
232
+ log_file.write(json.dumps(entry, ensure_ascii=False, sort_keys=True))
233
+ log_file.write("\n")
234
+
235
+ def _store_startup_index(self, xids: list[str]) -> None:
236
+ for xid in xids:
237
+ self._validate_xid(xid)
238
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
239
+ serialized = json.dumps(
240
+ {
241
+ "schema_version": 1,
242
+ "repository_fingerprint": self.repository_fingerprint,
243
+ "xids": xids,
244
+ },
245
+ ensure_ascii=False,
246
+ indent=2,
247
+ sort_keys=True,
248
+ )
249
+ self._write_atomic(self._startup_index_path(), serialized)
250
+
251
+ def _write_atomic(self, target: Path, serialized: str) -> None:
252
+ temporary_path: Path | None = None
253
+ try:
254
+ with tempfile.NamedTemporaryFile(
255
+ mode="w",
256
+ encoding="utf-8",
257
+ dir=self.cache_dir,
258
+ prefix=f".{target.stem}.",
259
+ suffix=".tmp",
260
+ delete=False,
261
+ ) as temporary:
262
+ temporary.write(serialized)
263
+ temporary_path = Path(temporary.name)
264
+ temporary_path.replace(target)
265
+ finally:
266
+ if temporary_path is not None and temporary_path.exists():
267
+ temporary_path.unlink()
268
+
269
+ def evict(self, xid: str) -> None:
270
+ path = self._entry_path(xid)
271
+ try:
272
+ path.unlink()
273
+ except FileNotFoundError:
274
+ pass
275
+
276
+ async def resolve(
277
+ self,
278
+ xid: str,
279
+ fetch_document: Callable[[str, str | None], Awaitable[dict[str, Any]]],
280
+ ) -> dict[str, Any]:
281
+ cached = self.load(xid)
282
+ known_version = cached["content_hash"] if cached else None
283
+ response = await fetch_document(xid, known_version)
284
+
285
+ if response.get("cache_status") == "not_modified":
286
+ try:
287
+ return self.materialize(response)
288
+ except DocumentCacheProtocolError:
289
+ response = await fetch_document(xid, None)
290
+
291
+ return self.materialize(response)
292
+
293
+ def materialize(self, response: dict[str, Any]) -> dict[str, Any]:
294
+ xid = str(response.get("xid", ""))
295
+ self._validate_xid(xid)
296
+ if (
297
+ response.get("repository_fingerprint")
298
+ != self.repository_fingerprint
299
+ ):
300
+ raise DocumentCacheProtocolError(
301
+ "response repository fingerprint does not match cache namespace"
302
+ )
303
+ if response.get("included_in_startup_contract_pack") is True:
304
+ result = dict(response)
305
+ result["client_cache_status"] = "startup_contract_pack"
306
+ return result
307
+ if response.get("cache_status") == "not_modified":
308
+ cached = self.load(xid)
309
+ if cached is None:
310
+ raise DocumentCacheProtocolError(
311
+ "document response omitted content without a usable cache entry"
312
+ )
313
+ result = dict(cached)
314
+ result.update(response)
315
+ result["content"] = cached["content"]
316
+ result["content_omitted"] = False
317
+ result["client_cache_status"] = "hit"
318
+ return result
319
+
320
+ if "content" not in response:
321
+ raise DocumentCacheProtocolError(
322
+ "document response omitted content without a usable cache entry"
323
+ )
324
+ stored = self.store(response)
325
+ result = dict(response)
326
+ result["client_cache_status"] = "refreshed" if stored else "bypassed"
327
+ return result
328
+
329
+ def _entry_path(self, xid: str) -> Path:
330
+ self._validate_xid(xid)
331
+ return self.cache_dir / f"{xid}.json"
332
+
333
+ def _startup_index_path(self) -> Path:
334
+ return self.cache_dir / "_startup.json"
335
+
336
+ @staticmethod
337
+ def _validate_xid(xid: str) -> None:
338
+ if not XID_PATTERN.fullmatch(xid):
339
+ raise ValueError(f"invalid XID for cache key: {xid!r}")
340
+
341
+
342
+ def _string_list(value: Any) -> list[str]:
343
+ if not isinstance(value, list):
344
+ return []
345
+ return [item for item in value if isinstance(item, str)]
346
+
347
+
348
+ def _unique_strings(values: Iterable[str]) -> list[str]:
349
+ seen: set[str] = set()
350
+ result: list[str] = []
351
+ for value in values:
352
+ if value in seen:
353
+ continue
354
+ seen.add(value)
355
+ result.append(value)
356
+ return result