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/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Portable XRefKit package namespace."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.3.0"
xrefkit/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
xrefkit/catalog_cli.py ADDED
@@ -0,0 +1,57 @@
1
+ """CLI for list-first catalogs and candidate maintenance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from .structure_catalog import get_entry, list_findings, list_targets, load_catalog, maintain_catalog, reconcile_receipts
10
+
11
+
12
+ DEFAULT_CATALOG = "knowledge/source_analysis/source_structure_catalog.yaml"
13
+
14
+
15
+ def main(argv: list[str] | None = None) -> int:
16
+ parser = argparse.ArgumentParser(prog="xrefkit catalog")
17
+ parser.add_argument("--root", default=".")
18
+ parser.add_argument("--catalog", default=DEFAULT_CATALOG)
19
+ sub = parser.add_subparsers(dest="command", required=True)
20
+ listing = sub.add_parser("list")
21
+ listing.add_argument("kind", choices=["targets", "findings"])
22
+ listing.add_argument("--target")
23
+ listing.add_argument("--json", action="store_true")
24
+ get = sub.add_parser("get")
25
+ get.add_argument("xid")
26
+ get.add_argument("--json", action="store_true")
27
+ maintain = sub.add_parser("maintain")
28
+ maintain.add_argument("--inbox", default="work/inbox/source_structure_findings")
29
+ maintain.add_argument("--apply-safe", action="store_true")
30
+ maintain.add_argument("--json", action="store_true")
31
+ reconcile = sub.add_parser("reconcile")
32
+ reconcile.add_argument("--reports", default="work/reports")
33
+ reconcile.add_argument("--inbox", default="work/inbox/source_structure_findings")
34
+ reconcile.add_argument("--json", action="store_true")
35
+ args = parser.parse_args(argv)
36
+ root = Path(args.root).resolve()
37
+ try:
38
+ if args.command == "list":
39
+ catalog = load_catalog(root / args.catalog)
40
+ if args.kind == "targets":
41
+ result = list_targets(catalog)
42
+ else:
43
+ if not args.target:
44
+ raise ValueError("--target is required for finding list")
45
+ result = list_findings(catalog, args.target)
46
+ elif args.command == "get":
47
+ result = get_entry(load_catalog(root / args.catalog), args.xid)
48
+ elif args.command == "maintain":
49
+ result = maintain_catalog(root, args.catalog, args.inbox, apply_safe=args.apply_safe)
50
+ else:
51
+ result = reconcile_receipts(root, args.reports, args.inbox)
52
+ except (OSError, KeyError, ValueError) as exc:
53
+ result = {"ok": False, "error": str(exc)}
54
+ print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
55
+ return 1
56
+ print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
57
+ return 0 if not isinstance(result, dict) or result.get("ok", True) else 1
xrefkit/cli.py ADDED
@@ -0,0 +1,71 @@
1
+ """Unified command dispatcher for the XRefKit package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import Sequence
7
+
8
+
9
+ _OPERATIONS = {"xref", "ctx", "goal", "gate", "pack", "dashboard", "skill"}
10
+ _V2 = {"package", "show"}
11
+
12
+
13
+ def _print_help() -> None:
14
+ print(
15
+ "usage: xrefkit <command> ...\n\n"
16
+ "commands:\n"
17
+ " init initialize or validate an XRefKit instance\n"
18
+ " xref manage XIDs and references\n"
19
+ " ctx build compact context packs\n"
20
+ " skill discover, validate, run, verify, and close Skills\n"
21
+ " tools list and run XID-backed client tools\n"
22
+ " catalog list and maintain Knowledge and structure catalogs\n"
23
+ " pack validate and build runtime/content packs\n"
24
+ " gate evaluate deterministic gates\n"
25
+ " goal manage desired-state Goals and continuation state\n"
26
+ " dashboard inspect runtime state\n"
27
+ " mcp start the integrated MCP server\n"
28
+ " package inspect installed Skill packages\n"
29
+ " show show effective Skill bundles"
30
+ )
31
+
32
+
33
+ def main(argv: Sequence[str] | None = None) -> int:
34
+ args = list(argv if argv is not None else sys.argv[1:])
35
+ if not args or args[0] in {"-h", "--help"}:
36
+ _print_help()
37
+ return 0
38
+
39
+ command = args[0]
40
+ if command == "pack" and len(args) > 1 and args[1] in {"build-base", "verify-base"}:
41
+ from .contracts import main as contracts_main
42
+
43
+ return contracts_main(args[1:])
44
+ if command in _OPERATIONS:
45
+ from .operations_cli import main as operations_main
46
+
47
+ return operations_main(args)
48
+ if command in _V2:
49
+ from .v2_cli import main as v2_main
50
+
51
+ return v2_main(args)
52
+ if command == "init":
53
+ from .instance import main as instance_main
54
+
55
+ return instance_main(args[1:])
56
+ if command == "tools":
57
+ from .tools import main as tools_main
58
+
59
+ return tools_main(args[1:])
60
+ if command == "catalog":
61
+ from .catalog_cli import main as catalog_main
62
+
63
+ return catalog_main(args[1:])
64
+ if command == "mcp":
65
+ from .mcp import main as mcp_main
66
+
67
+ return mcp_main(args[1:])
68
+
69
+ print(f"xrefkit: unknown command: {command}", file=sys.stderr)
70
+ _print_help()
71
+ return 2
xrefkit/contracts.py ADDED
@@ -0,0 +1,297 @@
1
+ """Compile and verify model-facing base runtime contract resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import re
10
+ import shutil
11
+ import tempfile
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import yaml
16
+
17
+
18
+ SCHEMA = "xrefkit.base_runtime/v1"
19
+ GENERATED_SCHEMA = "xrefkit.compiled_runtime/v1"
20
+ NORMATIVE_RE = re.compile(r"\bMUST(?:\s+NOT)?\b")
21
+
22
+
23
+ def _sha256(data: bytes) -> str:
24
+ return hashlib.sha256(data).hexdigest()
25
+
26
+
27
+ def _load_manifest(path: Path) -> dict[str, Any]:
28
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
29
+ if not isinstance(data, dict) or data.get("schema") != SCHEMA:
30
+ raise ValueError(f"unsupported base runtime manifest: {path}")
31
+ return data
32
+
33
+
34
+ def _estimate_tokens(text: str) -> int:
35
+ return (len(text) + 3) // 4
36
+
37
+
38
+ def _atomic_write_bytes(path: Path, data: bytes) -> None:
39
+ path.parent.mkdir(parents=True, exist_ok=True)
40
+ fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
41
+ try:
42
+ with os.fdopen(fd, "wb") as handle:
43
+ handle.write(data)
44
+ handle.flush()
45
+ os.fsync(handle.fileno())
46
+ os.replace(temporary, path)
47
+ except BaseException:
48
+ try:
49
+ os.unlink(temporary)
50
+ except FileNotFoundError:
51
+ pass
52
+ raise
53
+
54
+
55
+ def _publish_generation(output: Path, generated: dict[str, Any], model_body: str) -> None:
56
+ contracts_bytes = json.dumps(generated, ensure_ascii=False, indent=2, default=str).encode("utf-8") + b"\n"
57
+ model_bytes = model_body.encode("utf-8")
58
+ generation_id = hashlib.sha256(contracts_bytes + model_bytes).hexdigest()[:16]
59
+ generations = output / "generations"
60
+ generations.mkdir(parents=True, exist_ok=True)
61
+ generation_path = generations / generation_id
62
+ if not generation_path.exists():
63
+ temporary = Path(tempfile.mkdtemp(prefix=".generation-", dir=generations))
64
+ try:
65
+ (temporary / "contracts.json").write_bytes(contracts_bytes)
66
+ (temporary / "model_body.md").write_bytes(model_bytes)
67
+ os.replace(temporary, generation_path)
68
+ except FileExistsError:
69
+ shutil.rmtree(temporary, ignore_errors=True)
70
+ except BaseException:
71
+ shutil.rmtree(temporary, ignore_errors=True)
72
+ raise
73
+ pointer = {
74
+ "schema": "xrefkit.base_generation/v1",
75
+ "generation": generation_id,
76
+ "contracts": f"generations/{generation_id}/contracts.json",
77
+ "model_body": f"generations/{generation_id}/model_body.md",
78
+ }
79
+ # The pointer is the publication boundary. Fixed files remain compatibility snapshots.
80
+ _atomic_write_bytes(output / "contracts.json", contracts_bytes)
81
+ _atomic_write_bytes(output / "model_body.md", model_bytes)
82
+ _atomic_write_bytes(
83
+ output / "current.json",
84
+ json.dumps(pointer, ensure_ascii=False, indent=2).encode("utf-8") + b"\n",
85
+ )
86
+
87
+
88
+ def _published_paths(output: Path) -> tuple[Path, Path]:
89
+ pointer_path = output / "current.json"
90
+ if not pointer_path.is_file():
91
+ raise ValueError("base runtime generation pointer current.json is required")
92
+ pointer = json.loads(pointer_path.read_text(encoding="utf-8"))
93
+ generation = str(pointer["generation"])
94
+ if not re.fullmatch(r"[0-9a-f]{16}", generation):
95
+ raise ValueError("invalid base runtime generation")
96
+ generation_path = output / "generations" / generation
97
+ return generation_path / "contracts.json", generation_path / "model_body.md"
98
+
99
+
100
+ def compile_base_runtime(
101
+ repo_root: str | Path,
102
+ manifest_path: str | Path,
103
+ output_dir: str | Path,
104
+ ) -> dict[str, Any]:
105
+ result = _compile_base_runtime(repo_root, manifest_path, output_dir)
106
+ output = Path(output_dir)
107
+ if not output.is_absolute():
108
+ output = Path(repo_root).resolve() / output
109
+ generated = {key: value for key, value in result.items() if key != "model_body"}
110
+ _publish_generation(output, generated, result["model_body"])
111
+ return {key: value for key, value in result.items() if key != "model_body"}
112
+
113
+
114
+ def _compile_base_runtime(
115
+ repo_root: str | Path,
116
+ manifest_path: str | Path,
117
+ output_dir: str | Path,
118
+ ) -> dict[str, Any]:
119
+ root = Path(repo_root).resolve()
120
+ manifest_file = Path(manifest_path)
121
+ if not manifest_file.is_absolute():
122
+ manifest_file = root / manifest_file
123
+ manifest = _load_manifest(manifest_file)
124
+ sources: dict[str, dict[str, Any]] = {}
125
+ source_texts: dict[str, str] = {}
126
+ for source in manifest.get("sources", []):
127
+ xid = str(source["xid"])
128
+ path = root / str(source["path"])
129
+ body = path.read_text(encoding="utf-8")
130
+ if f"xid: {xid}" not in body:
131
+ raise ValueError(f"source XID mismatch: {path} expected {xid}")
132
+ sources[xid] = {
133
+ "xid": xid,
134
+ "path": str(source["path"]),
135
+ "source_hash": _sha256(body.encode("utf-8")),
136
+ }
137
+ source_texts[xid] = body
138
+
139
+ obligations: list[dict[str, str]] = []
140
+ seen_ids: set[str] = set()
141
+ covered_lines: dict[str, set[str]] = {xid: set() for xid in sources}
142
+ for item in manifest.get("obligations", []):
143
+ obligation_id = str(item["id"])
144
+ if obligation_id in seen_ids:
145
+ raise ValueError(f"duplicate obligation id: {obligation_id}")
146
+ seen_ids.add(obligation_id)
147
+ source_xid = str(item["source_xid"])
148
+ if source_xid not in source_texts:
149
+ raise ValueError(f"unknown obligation source XID: {source_xid}")
150
+ source_match = str(item["source_match"])
151
+ matching = [line.strip() for line in source_texts[source_xid].splitlines() if source_match in line]
152
+ if not matching:
153
+ raise ValueError(f"obligation source_match not found: {obligation_id}")
154
+ covered_lines[source_xid].update(matching)
155
+ obligations.append(
156
+ {
157
+ "id": obligation_id,
158
+ "source_xid": source_xid,
159
+ "level": str(item["level"]),
160
+ "statement": str(item["statement"]),
161
+ }
162
+ )
163
+
164
+ lint_candidates: list[dict[str, Any]] = []
165
+ for xid, body in source_texts.items():
166
+ for line_number, line in enumerate(body.splitlines(), start=1):
167
+ stripped = line.strip()
168
+ if NORMATIVE_RE.search(stripped) and stripped not in covered_lines[xid]:
169
+ lint_candidates.append(
170
+ {
171
+ "source_xid": xid,
172
+ "line": line_number,
173
+ "text": stripped,
174
+ "status": "pending",
175
+ }
176
+ )
177
+
178
+ body_lines = ["# XRefKit Base Runtime Contract", ""]
179
+ for item in obligations:
180
+ body_lines.append(f"- [{item['level']}] {item['id']}: {item['statement']}")
181
+ conditional = manifest.get("conditional_loads", [])
182
+ if conditional:
183
+ body_lines.extend(["", "Conditional loads:"])
184
+ for item in conditional:
185
+ body_lines.append(f"- {item['when']}: xid {item['xid']}")
186
+ model_body = "\n".join(body_lines) + "\n"
187
+ budgets = dict(manifest.get("budgets", {}))
188
+ estimated_tokens = _estimate_tokens(model_body)
189
+ approval = dict(manifest.get("approval", {}))
190
+ result = {
191
+ "schema": GENERATED_SCHEMA,
192
+ "profile": manifest.get("profile", "model-compact"),
193
+ "compiler_version": manifest.get("compiler_version", 1),
194
+ "manifest_path": str(manifest_file.relative_to(root)),
195
+ "manifest_hash": _sha256(manifest_file.read_bytes()),
196
+ "approval": approval,
197
+ "estimator": manifest.get("estimator", {}),
198
+ "budgets": budgets,
199
+ "sources": list(sources.values()),
200
+ "obligations": obligations,
201
+ "conditional_loads": conditional,
202
+ "lint_candidates": lint_candidates,
203
+ "model_body_hash": _sha256(model_body.encode("utf-8")),
204
+ "estimated_tokens": estimated_tokens,
205
+ "release_ready": approval.get("status") == "accepted" and not lint_candidates,
206
+ "model_body": model_body,
207
+ }
208
+ return result
209
+
210
+
211
+ def verify_base_runtime(
212
+ repo_root: str | Path,
213
+ manifest_path: str | Path,
214
+ output_dir: str | Path,
215
+ *,
216
+ release: bool,
217
+ ) -> dict[str, Any]:
218
+ compiled = _compile_base_runtime(repo_root, manifest_path, output_dir)
219
+ result = {key: value for key, value in compiled.items() if key != "model_body"}
220
+ errors: list[str] = []
221
+ generated_path = Path(output_dir)
222
+ if not generated_path.is_absolute():
223
+ generated_path = Path(repo_root).resolve() / generated_path
224
+ try:
225
+ contracts_path, model_body_path = _published_paths(generated_path)
226
+ except (OSError, UnicodeDecodeError, KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
227
+ errors.append(f"compiled generation pointer cannot be read: {exc}")
228
+ return {**result, "ok": False, "errors": errors, "release": release}
229
+ if not contracts_path.is_file() or not model_body_path.is_file():
230
+ errors.append("compiled runtime output is missing")
231
+ return {**result, "ok": False, "errors": errors, "release": release}
232
+ try:
233
+ generated = json.loads(contracts_path.read_text(encoding="utf-8"))
234
+ except (OSError, json.JSONDecodeError) as exc:
235
+ errors.append(f"compiled contracts cannot be read: {exc}")
236
+ return {**result, "ok": False, "errors": errors, "release": release}
237
+ expected = json.loads(
238
+ json.dumps(
239
+ {key: value for key, value in compiled.items() if key != "model_body"},
240
+ ensure_ascii=False,
241
+ default=str,
242
+ )
243
+ )
244
+ if generated != expected:
245
+ errors.append("compiled contracts differ from current source and manifest")
246
+ try:
247
+ published_model_body = model_body_path.read_text(encoding="utf-8")
248
+ except (OSError, UnicodeDecodeError) as exc:
249
+ errors.append(f"compiled model body cannot be read: {exc}")
250
+ published_model_body = None
251
+ if published_model_body is not None and published_model_body != compiled["model_body"]:
252
+ errors.append("compiled model body differs from current source and manifest")
253
+ l0_budget = int(result["budgets"].get("l0_tokens", 0))
254
+ if not l0_budget or result["estimated_tokens"] > l0_budget:
255
+ errors.append(
256
+ f"L0 token budget exceeded: {result['estimated_tokens']} > {l0_budget}"
257
+ )
258
+ if release:
259
+ if result["approval"].get("status") != "accepted":
260
+ errors.append("runtime budget and manifest approval is not accepted")
261
+ if result["lint_candidates"]:
262
+ errors.append(f"pending normative lint candidates: {len(result['lint_candidates'])}")
263
+ return {**result, "ok": not errors, "errors": errors, "release": release}
264
+
265
+
266
+ def main(argv: list[str] | None = None) -> int:
267
+ parser = argparse.ArgumentParser(prog="xrefkit pack")
268
+ sub = parser.add_subparsers(dest="command", required=True)
269
+ for name in ("build-base", "verify-base"):
270
+ command = sub.add_parser(name)
271
+ command.add_argument("--root", default=".")
272
+ command.add_argument("--manifest", default="docs/core/contracts/base_runtime_manifest.yaml")
273
+ command.add_argument("--output", default="xrefkit/resources/base")
274
+ command.add_argument("--json", action="store_true")
275
+ if name == "verify-base":
276
+ command.add_argument("--draft", action="store_true")
277
+ args = parser.parse_args(argv)
278
+ try:
279
+ if args.command == "build-base":
280
+ result = compile_base_runtime(args.root, args.manifest, args.output)
281
+ result = {**result, "ok": True}
282
+ else:
283
+ result = verify_base_runtime(
284
+ args.root,
285
+ args.manifest,
286
+ args.output,
287
+ release=not args.draft,
288
+ )
289
+ except (OSError, ValueError, yaml.YAMLError) as exc:
290
+ result = {"ok": False, "errors": [str(exc)]}
291
+ if args.json:
292
+ print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
293
+ else:
294
+ print("ok" if result.get("ok") else "failed")
295
+ for error in result.get("errors", []):
296
+ print(f"- {error}")
297
+ return 0 if result.get("ok") else 1
xrefkit/ctx.py ADDED
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from xrefkit.xref import MANAGED_FRAGMENT_RE, WIKI_XREF_RE, XrefConfig, build_index
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class PackedDoc:
13
+ xid: str
14
+ path: str
15
+ title: str | None
16
+ bytes_included: int
17
+
18
+
19
+ def _extract_referenced_xids(text: str) -> set[str]:
20
+ xids = set(MANAGED_FRAGMENT_RE.findall(text))
21
+ xids.update(WIKI_XREF_RE.findall(text))
22
+ return xids
23
+
24
+
25
+ def _read_text(path: Path) -> str:
26
+ return path.read_text(encoding="utf-8")
27
+
28
+
29
+ def _build_pack_text(
30
+ *,
31
+ root: Path,
32
+ ordered_xids: list[str],
33
+ index,
34
+ per_doc_bytes: int,
35
+ max_bytes: int,
36
+ ) -> tuple[str, list[PackedDoc]]:
37
+ parts: list[str] = []
38
+ packed: list[PackedDoc] = []
39
+
40
+ header = [
41
+ "# Context Pack",
42
+ "",
43
+ f"- root: {root.as_posix()}",
44
+ f"- docs: {len(ordered_xids)}",
45
+ "",
46
+ ]
47
+ parts.append("\n".join(header))
48
+
49
+ total = sum(len(p.encode("utf-8")) for p in parts)
50
+ for xid in ordered_xids:
51
+ info = index.get(xid)
52
+ if info is None:
53
+ continue
54
+ rel = str(info.path.relative_to(root)).replace("\\", "/")
55
+ title = info.title
56
+
57
+ text = _read_text(info.path)
58
+ blob = text.encode("utf-8")
59
+ truncated = blob[:per_doc_bytes].decode("utf-8", errors="ignore")
60
+
61
+ section = [
62
+ "---",
63
+ f"## {title or xid}",
64
+ f"- xid: {xid}",
65
+ f"- path: {rel}",
66
+ "",
67
+ truncated.rstrip("\n"),
68
+ "",
69
+ ]
70
+ section_text = "\n".join(section)
71
+ section_bytes = len(section_text.encode("utf-8"))
72
+ if total + section_bytes > max_bytes:
73
+ break
74
+ parts.append(section_text)
75
+ total += section_bytes
76
+ packed.append(
77
+ PackedDoc(
78
+ xid=xid,
79
+ path=rel,
80
+ title=title,
81
+ bytes_included=len(truncated.encode("utf-8")),
82
+ )
83
+ )
84
+
85
+ return "\n".join(parts).rstrip() + "\n", packed
86
+
87
+
88
+ def ctx_pack(
89
+ cfg: XrefConfig,
90
+ *,
91
+ seeds: list[str],
92
+ depth: int,
93
+ max_bytes: int,
94
+ per_doc_bytes: int,
95
+ ) -> dict[str, object]:
96
+ root = cfg.resolved_root()
97
+ index, issues = build_index(cfg)
98
+
99
+ visited: set[str] = set()
100
+ frontier: set[str] = {s for s in seeds if s in index}
101
+ ordered: list[str] = []
102
+
103
+ for _ in range(max(0, depth) + 1):
104
+ if not frontier:
105
+ break
106
+ next_frontier: set[str] = set()
107
+ for xid in sorted(frontier):
108
+ if xid in visited:
109
+ continue
110
+ visited.add(xid)
111
+ ordered.append(xid)
112
+ text = _read_text(index[xid].path)
113
+ for ref in _extract_referenced_xids(text):
114
+ if ref in index and ref not in visited:
115
+ next_frontier.add(ref)
116
+ frontier = next_frontier
117
+
118
+ pack_text, packed_docs = _build_pack_text(
119
+ root=root,
120
+ ordered_xids=ordered,
121
+ index=index,
122
+ per_doc_bytes=per_doc_bytes,
123
+ max_bytes=max_bytes,
124
+ )
125
+
126
+ return {
127
+ "issues": issues,
128
+ "seeds": seeds,
129
+ "depth": depth,
130
+ "docs": [d.__dict__ for d in packed_docs],
131
+ "text": pack_text,
132
+ }
133
+
134
+
135
+ def cmd_ctx(args: argparse.Namespace, cfg: XrefConfig) -> int:
136
+ if args.ctx_cmd != "pack":
137
+ raise ValueError(f"Unknown ctx command: {args.ctx_cmd}")
138
+
139
+ result = ctx_pack(
140
+ cfg,
141
+ seeds=list(args.seed or []),
142
+ depth=int(args.depth),
143
+ max_bytes=int(args.max_bytes),
144
+ per_doc_bytes=int(args.per_doc_bytes),
145
+ )
146
+
147
+ out_path = Path(args.out).resolve() if args.out else None
148
+ if out_path is not None:
149
+ out_path.parent.mkdir(parents=True, exist_ok=True)
150
+ out_path.write_text(result["text"], encoding="utf-8", newline="\n")
151
+
152
+ if args.json:
153
+ if out_path is not None:
154
+ result["out"] = str(out_path)
155
+ print(json.dumps(result, ensure_ascii=False, indent=2))
156
+ else:
157
+ print(result["text"], end="")
158
+
159
+ return 0
160
+