xrefkit 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xrefkit/__init__.py +5 -0
- xrefkit/__main__.py +5 -0
- xrefkit/catalog_cli.py +57 -0
- xrefkit/cli.py +71 -0
- xrefkit/contracts.py +297 -0
- xrefkit/ctx.py +160 -0
- xrefkit/dashboard.py +1220 -0
- xrefkit/discovery.py +85 -0
- xrefkit/gate.py +428 -0
- xrefkit/goalstate.py +555 -0
- xrefkit/hashing.py +18 -0
- xrefkit/import_skill.py +469 -0
- xrefkit/instance.py +133 -0
- xrefkit/loaders.py +77 -0
- xrefkit/mcp/__init__.py +26 -0
- xrefkit/mcp/audit.py +168 -0
- xrefkit/mcp/bootstrap.py +337 -0
- xrefkit/mcp/catalog.py +2638 -0
- xrefkit/mcp/cli.py +173 -0
- xrefkit/mcp/client_cache.py +356 -0
- xrefkit/mcp/context_registry.py +277 -0
- xrefkit/mcp/contracts.py +243 -0
- xrefkit/mcp/dist.py +234 -0
- xrefkit/mcp/ownership.py +246 -0
- xrefkit/mcp/repository.py +217 -0
- xrefkit/mcp/schemas.py +349 -0
- xrefkit/mcp/server.py +773 -0
- xrefkit/mcp/startup_contract_pack.py +154 -0
- xrefkit/mcp_tools.py +47 -0
- xrefkit/models/__init__.py +41 -0
- xrefkit/models/common.py +185 -0
- xrefkit/models/effective_bundle.py +131 -0
- xrefkit/models/local_manifest.py +217 -0
- xrefkit/models/package_manifest.py +126 -0
- xrefkit/models/run_log.py +276 -0
- xrefkit/models/server_config.py +160 -0
- xrefkit/models/skill_definition.py +131 -0
- xrefkit/operations_cli.py +670 -0
- xrefkit/ownership.py +276 -0
- xrefkit/packmeta.py +289 -0
- xrefkit/registry.py +334 -0
- xrefkit/resolver.py +252 -0
- xrefkit/resource_provider.py +187 -0
- xrefkit/resources/base/contracts.json +178 -0
- xrefkit/resources/base/current.json +6 -0
- xrefkit/resources/base/generations/7a682a5272907354/contracts.json +178 -0
- xrefkit/resources/base/generations/7a682a5272907354/model_body.md +22 -0
- xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +178 -0
- xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +22 -0
- xrefkit/resources/base/model_body.md +22 -0
- xrefkit/runlog.py +45 -0
- xrefkit/skillmeta.py +1034 -0
- xrefkit/skillrun.py +2381 -0
- xrefkit/structure_catalog.py +199 -0
- xrefkit/tools/__init__.py +119 -0
- xrefkit/tools/__main__.py +4 -0
- xrefkit/v2_cli.py +130 -0
- xrefkit/workspace.py +117 -0
- xrefkit/xref.py +1048 -0
- xrefkit-0.3.0.dist-info/METADATA +203 -0
- xrefkit-0.3.0.dist-info/RECORD +65 -0
- xrefkit-0.3.0.dist-info/WHEEL +5 -0
- xrefkit-0.3.0.dist-info/entry_points.txt +2 -0
- xrefkit-0.3.0.dist-info/licenses/LICENSE +21 -0
- xrefkit-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""List-first source target/finding catalog and safe candidate promotion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SCHEMA = "xrefkit.source_structure_catalog/v1"
|
|
15
|
+
RECEIPT_SCHEMA = "xrefkit.source_structure_candidate/v1"
|
|
16
|
+
XID_RE = re.compile(r"^[A-F0-9]{12}$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_catalog(path: str | Path) -> dict[str, Any]:
|
|
20
|
+
data = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
|
|
21
|
+
if not isinstance(data, dict) or data.get("schema") != SCHEMA:
|
|
22
|
+
raise ValueError("unsupported source structure catalog")
|
|
23
|
+
return data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_catalog(path: str | Path, catalog: dict[str, Any]) -> None:
|
|
27
|
+
Path(path).write_text(yaml.safe_dump(catalog, sort_keys=False, allow_unicode=True), encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _safe_value(value: Any) -> Any:
|
|
31
|
+
if isinstance(value, (date, datetime)):
|
|
32
|
+
return value.isoformat()
|
|
33
|
+
if isinstance(value, list):
|
|
34
|
+
return [_safe_value(item) for item in value]
|
|
35
|
+
if isinstance(value, dict):
|
|
36
|
+
return {key: _safe_value(item) for key, item in value.items()}
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def list_targets(catalog: dict[str, Any]) -> list[dict[str, Any]]:
|
|
41
|
+
return [_safe_value(dict(item)) for item in catalog.get("targets", [])]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def list_findings(catalog: dict[str, Any], target_xid: str) -> list[dict[str, Any]]:
|
|
45
|
+
key = target_xid.upper()
|
|
46
|
+
return [_safe_value(dict(item)) for item in catalog.get("findings", []) if str(item.get("target_xid", "")).upper() == key]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_entry(catalog: dict[str, Any], xid: str) -> dict[str, Any]:
|
|
50
|
+
key = xid.upper()
|
|
51
|
+
for collection, field in (("targets", "target_xid"), ("findings", "finding_xid")):
|
|
52
|
+
for item in catalog.get(collection, []):
|
|
53
|
+
if str(item.get(field, "")).upper() == key:
|
|
54
|
+
return _safe_value({"kind": collection[:-1], **dict(item)})
|
|
55
|
+
raise KeyError(f"unknown catalog XID: {xid}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _target_identity(item: dict[str, Any]) -> tuple[str, str, str]:
|
|
59
|
+
return (
|
|
60
|
+
str(item.get("repository_identity", "")).strip(),
|
|
61
|
+
str(item.get("source_scope", "")).strip(),
|
|
62
|
+
str(item.get("target_kind", "")).strip(),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _derived_xid(*parts: str) -> str:
|
|
67
|
+
return hashlib.sha256("\x1f".join(parts).encode("utf-8")).hexdigest()[:12].upper()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate_receipt(receipt: dict[str, Any], root: Path) -> list[str]:
|
|
71
|
+
errors: list[str] = []
|
|
72
|
+
if receipt.get("schema") != RECEIPT_SCHEMA:
|
|
73
|
+
errors.append("unsupported receipt schema")
|
|
74
|
+
required = (
|
|
75
|
+
"candidate_xid", "artifact_path", "repository_identity", "source_scope",
|
|
76
|
+
"target_kind", "source_revision", "producer_skill", "analyzed_at",
|
|
77
|
+
)
|
|
78
|
+
for field in required:
|
|
79
|
+
if not str(receipt.get(field, "")).strip():
|
|
80
|
+
errors.append(f"missing {field}")
|
|
81
|
+
xid = str(receipt.get("candidate_xid", "")).upper()
|
|
82
|
+
if xid and not XID_RE.fullmatch(xid):
|
|
83
|
+
errors.append("candidate_xid must be 12 hexadecimal characters")
|
|
84
|
+
artifact = root / str(receipt.get("artifact_path", ""))
|
|
85
|
+
if not artifact.is_file():
|
|
86
|
+
errors.append(f"artifact not found: {artifact}")
|
|
87
|
+
return errors
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def maintain_catalog(
|
|
91
|
+
root: str | Path,
|
|
92
|
+
catalog_path: str | Path,
|
|
93
|
+
inbox: str | Path,
|
|
94
|
+
*,
|
|
95
|
+
apply_safe: bool,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
root_path = Path(root).resolve()
|
|
98
|
+
catalog_file = root_path / catalog_path
|
|
99
|
+
catalog = load_catalog(catalog_file)
|
|
100
|
+
results: list[dict[str, Any]] = []
|
|
101
|
+
changed = False
|
|
102
|
+
for receipt_path in sorted((root_path / inbox).glob("*.yaml")):
|
|
103
|
+
receipt = yaml.safe_load(receipt_path.read_text(encoding="utf-8"))
|
|
104
|
+
if not isinstance(receipt, dict):
|
|
105
|
+
results.append({"receipt": str(receipt_path), "status": "invalid", "errors": ["receipt is not a mapping"]})
|
|
106
|
+
continue
|
|
107
|
+
errors = _validate_receipt(receipt, root_path)
|
|
108
|
+
identity = _target_identity(receipt)
|
|
109
|
+
matches = [item for item in catalog.get("targets", []) if _target_identity(item) == identity]
|
|
110
|
+
if len(matches) > 1:
|
|
111
|
+
errors.append("multiple targets match receipt identity")
|
|
112
|
+
finding_xid = str(receipt.get("candidate_xid", "")).upper()
|
|
113
|
+
if any(str(item.get("finding_xid", "")).upper() == finding_xid for item in catalog.get("findings", [])):
|
|
114
|
+
results.append({"receipt": str(receipt_path), "status": "already_registered", "finding_xid": finding_xid})
|
|
115
|
+
continue
|
|
116
|
+
if errors or not apply_safe:
|
|
117
|
+
results.append({
|
|
118
|
+
"receipt": str(receipt_path),
|
|
119
|
+
"status": "pending_review" if not errors else "invalid",
|
|
120
|
+
"errors": errors,
|
|
121
|
+
})
|
|
122
|
+
continue
|
|
123
|
+
if matches:
|
|
124
|
+
target = matches[0]
|
|
125
|
+
else:
|
|
126
|
+
target_xid = str(receipt.get("target_xid", "")).upper() or _derived_xid(*identity)
|
|
127
|
+
target = {
|
|
128
|
+
"target_xid": target_xid,
|
|
129
|
+
"name": str(receipt.get("target_name") or receipt.get("target_hint") or identity[1]),
|
|
130
|
+
"repository_identity": identity[0],
|
|
131
|
+
"source_scope": identity[1],
|
|
132
|
+
"target_kind": identity[2],
|
|
133
|
+
"aliases": list(receipt.get("aliases") or []),
|
|
134
|
+
}
|
|
135
|
+
catalog.setdefault("targets", []).append(target)
|
|
136
|
+
artifact = root_path / str(receipt["artifact_path"])
|
|
137
|
+
slug = re.sub(r"[^a-z0-9]+", "_", str(target["name"]).lower()).strip("_") or "target"
|
|
138
|
+
detail_relative = Path("knowledge/source_analysis") / f"{finding_xid.lower()}_{slug}_structure_findings.md"
|
|
139
|
+
detail = root_path / detail_relative
|
|
140
|
+
body = artifact.read_text(encoding="utf-8")
|
|
141
|
+
if f"xid: {finding_xid}" not in body:
|
|
142
|
+
body = f'<!-- xid: {finding_xid} -->\n<a id="xid-{finding_xid}"></a>\n\n' + body
|
|
143
|
+
detail.parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
detail.write_text(body, encoding="utf-8")
|
|
145
|
+
finding = {
|
|
146
|
+
"finding_xid": finding_xid,
|
|
147
|
+
"target_xid": target["target_xid"],
|
|
148
|
+
"title": str(receipt.get("title") or f"{target['name']} structure findings"),
|
|
149
|
+
"analysis_kinds": list(receipt.get("analysis_kinds") or ["source_structure"]),
|
|
150
|
+
"status": "current",
|
|
151
|
+
"source_revision": str(receipt["source_revision"]),
|
|
152
|
+
"last_verified": str(receipt["analyzed_at"]),
|
|
153
|
+
"coverage": str(receipt.get("coverage") or "See canonical finding detail."),
|
|
154
|
+
"unresolved_verification": str(receipt.get("unresolved_verification") or "None recorded."),
|
|
155
|
+
"detail_path": detail_relative.as_posix(),
|
|
156
|
+
}
|
|
157
|
+
catalog.setdefault("findings", []).append(finding)
|
|
158
|
+
receipt["status"] = "registered"
|
|
159
|
+
receipt["target_xid"] = target["target_xid"]
|
|
160
|
+
receipt["canonical_detail_path"] = detail_relative.as_posix()
|
|
161
|
+
receipt_path.write_text(yaml.safe_dump(receipt, sort_keys=False, allow_unicode=True), encoding="utf-8")
|
|
162
|
+
results.append({"receipt": str(receipt_path), **finding, "processing_status": "registered"})
|
|
163
|
+
changed = True
|
|
164
|
+
if changed:
|
|
165
|
+
save_catalog(catalog_file, catalog)
|
|
166
|
+
return {
|
|
167
|
+
"ok": all(item["status"] not in {"invalid"} for item in results),
|
|
168
|
+
"changed": changed,
|
|
169
|
+
"results": results,
|
|
170
|
+
"registered": sum(item.get("processing_status") == "registered" for item in results),
|
|
171
|
+
"pending_review": sum(item["status"] == "pending_review" for item in results),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def reconcile_receipts(root: str | Path, reports: str | Path, inbox: str | Path) -> dict[str, Any]:
|
|
176
|
+
root_path = Path(root).resolve()
|
|
177
|
+
inbox_path = root_path / inbox
|
|
178
|
+
inbox_path.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
existing = {
|
|
180
|
+
str(data.get("artifact_path"))
|
|
181
|
+
for path in inbox_path.glob("*.yaml")
|
|
182
|
+
if isinstance((data := yaml.safe_load(path.read_text(encoding="utf-8"))), dict)
|
|
183
|
+
}
|
|
184
|
+
created: list[str] = []
|
|
185
|
+
pattern = re.compile(r"^candidate_xid:\s*([A-Fa-f0-9]{12})\s*$", re.MULTILINE)
|
|
186
|
+
for report in sorted((root_path / reports).glob("*.md")):
|
|
187
|
+
relative = report.relative_to(root_path).as_posix()
|
|
188
|
+
if relative in existing:
|
|
189
|
+
continue
|
|
190
|
+
text = report.read_text(encoding="utf-8")
|
|
191
|
+
match = pattern.search(text[:2000])
|
|
192
|
+
if not match:
|
|
193
|
+
continue
|
|
194
|
+
xid = match.group(1).upper()
|
|
195
|
+
receipt = {"schema": RECEIPT_SCHEMA, "candidate_xid": xid, "artifact_path": relative, "status": "incomplete"}
|
|
196
|
+
target = inbox_path / f"{date.today().isoformat()}_{xid}.yaml"
|
|
197
|
+
target.write_text(yaml.safe_dump(receipt, sort_keys=False), encoding="utf-8")
|
|
198
|
+
created.append(str(target))
|
|
199
|
+
return {"ok": True, "created": created, "count": len(created)}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Stable tool-ID registry and client-side execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ToolContract:
|
|
19
|
+
tool_id: str
|
|
20
|
+
xid: str
|
|
21
|
+
path: str
|
|
22
|
+
execution_location: str
|
|
23
|
+
side_effects: str
|
|
24
|
+
content_hash: str
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, str]:
|
|
27
|
+
return {
|
|
28
|
+
"tool_id": self.tool_id,
|
|
29
|
+
"xid": self.xid,
|
|
30
|
+
"path": self.path,
|
|
31
|
+
"execution_location": self.execution_location,
|
|
32
|
+
"side_effects": self.side_effects,
|
|
33
|
+
"content_hash": self.content_hash,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_tool_contracts(root: str | Path = ".", manifest: str = "tools/contracts.yaml") -> list[ToolContract]:
|
|
38
|
+
root_path = Path(root).resolve()
|
|
39
|
+
data = yaml.safe_load((root_path / manifest).read_text(encoding="utf-8"))
|
|
40
|
+
if not isinstance(data, dict) or data.get("schema") != "xrefkit.tool_contracts/v1":
|
|
41
|
+
raise ValueError("unsupported tool contract manifest")
|
|
42
|
+
contracts: list[ToolContract] = []
|
|
43
|
+
ids: set[str] = set()
|
|
44
|
+
xids: set[str] = set()
|
|
45
|
+
for item in data.get("tools", []):
|
|
46
|
+
tool_id = str(item["tool_id"])
|
|
47
|
+
xid = str(item["xid"]).upper()
|
|
48
|
+
if tool_id in ids:
|
|
49
|
+
raise ValueError(f"duplicate tool_id: {tool_id}")
|
|
50
|
+
if xid in xids:
|
|
51
|
+
raise ValueError(f"duplicate tool XID: {xid}")
|
|
52
|
+
path = root_path / str(item["path"])
|
|
53
|
+
if not path.is_file():
|
|
54
|
+
raise ValueError(f"tool path not found: {path}")
|
|
55
|
+
ids.add(tool_id)
|
|
56
|
+
xids.add(xid)
|
|
57
|
+
contracts.append(
|
|
58
|
+
ToolContract(
|
|
59
|
+
tool_id=tool_id,
|
|
60
|
+
xid=xid,
|
|
61
|
+
path=str(item["path"]),
|
|
62
|
+
execution_location=str(item["execution_location"]),
|
|
63
|
+
side_effects=str(item["side_effects"]),
|
|
64
|
+
content_hash=hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
return contracts
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _find(contracts: list[ToolContract], identity: str) -> ToolContract:
|
|
71
|
+
key = identity.upper()
|
|
72
|
+
for contract in contracts:
|
|
73
|
+
if contract.tool_id == identity or contract.xid == key:
|
|
74
|
+
return contract
|
|
75
|
+
raise KeyError(f"unknown tool: {identity}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main(argv: list[str] | None = None) -> int:
|
|
79
|
+
parser = argparse.ArgumentParser(prog="xrefkit tools")
|
|
80
|
+
parser.add_argument("--root", default=".")
|
|
81
|
+
parser.add_argument("--manifest", default="tools/contracts.yaml")
|
|
82
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
83
|
+
list_parser = sub.add_parser("list")
|
|
84
|
+
list_parser.add_argument("--json", action="store_true")
|
|
85
|
+
show = sub.add_parser("show")
|
|
86
|
+
show.add_argument("tool")
|
|
87
|
+
show.add_argument("--json", action="store_true")
|
|
88
|
+
run = sub.add_parser("run")
|
|
89
|
+
run.add_argument("tool")
|
|
90
|
+
run.add_argument("tool_args", nargs=argparse.REMAINDER)
|
|
91
|
+
args = parser.parse_args(argv)
|
|
92
|
+
try:
|
|
93
|
+
contracts = load_tool_contracts(args.root, args.manifest)
|
|
94
|
+
if args.command == "list":
|
|
95
|
+
payload: Any = [contract.to_dict() for contract in contracts]
|
|
96
|
+
else:
|
|
97
|
+
contract = _find(contracts, args.tool)
|
|
98
|
+
if args.command == "show":
|
|
99
|
+
payload = contract.to_dict()
|
|
100
|
+
else:
|
|
101
|
+
if contract.execution_location != "client":
|
|
102
|
+
raise ValueError(f"tool is not client executable: {contract.tool_id}")
|
|
103
|
+
command = [sys.executable, str(Path(args.root) / contract.path), *args.tool_args]
|
|
104
|
+
return subprocess.run(command, check=False).returncode
|
|
105
|
+
except (OSError, KeyError, ValueError, yaml.YAMLError) as exc:
|
|
106
|
+
print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
|
|
107
|
+
return 1
|
|
108
|
+
as_json = bool(getattr(args, "json", False))
|
|
109
|
+
if as_json:
|
|
110
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
111
|
+
elif isinstance(payload, list):
|
|
112
|
+
for item in payload:
|
|
113
|
+
print(f"{item['tool_id']}\t{item['xid']}\t{item['execution_location']}")
|
|
114
|
+
else:
|
|
115
|
+
print(f"{payload['tool_id']}\t{payload['xid']}\t{payload['path']}")
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = ["ToolContract", "load_tool_contracts", "main"]
|
xrefkit/v2_cli.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Minimal CLI for XRefKit v2 MVP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from typing import Sequence
|
|
8
|
+
|
|
9
|
+
from .discovery import discover_skill_packages, package_list_rows
|
|
10
|
+
from .loaders import load_server_config
|
|
11
|
+
from .resolver import EffectiveSkillResolver
|
|
12
|
+
from .workspace import build_registry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _add_workspace_args(parser: argparse.ArgumentParser) -> None:
|
|
16
|
+
parser.add_argument("--package-manifest", action="append", default=[], help="Path to package_manifest.yaml")
|
|
17
|
+
parser.add_argument("--local-manifest", help="Path to xrefkit.local/local_manifest.yaml")
|
|
18
|
+
parser.add_argument("--enable-entry-point-discovery", action="store_true", help="Load enabled packages from Python entry points")
|
|
19
|
+
parser.add_argument("--enabled-package", action="append", default=[], help="Package id enabled for resolver use")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _enabled_packages_from_args(args: argparse.Namespace) -> set[str]:
|
|
23
|
+
enabled = set(args.enabled_package or [])
|
|
24
|
+
server_config = getattr(args, "server_config", None)
|
|
25
|
+
if server_config:
|
|
26
|
+
config = load_server_config(server_config)
|
|
27
|
+
enabled.update(config.packages.enabled)
|
|
28
|
+
return enabled
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _bundle_tree(bundle: object) -> str:
|
|
32
|
+
# Deliberately use model_dump so this function stays stable if the model
|
|
33
|
+
# gains computed properties later.
|
|
34
|
+
data = bundle.model_dump(mode="json") # type: ignore[attr-defined]
|
|
35
|
+
lines = [f"effective_skill: {data['effective_skill_id']}", f"mode: {data['resolution_mode']}"]
|
|
36
|
+
lines.append("loaded:")
|
|
37
|
+
for group, entries in data["loaded_texts"].items():
|
|
38
|
+
if not entries:
|
|
39
|
+
continue
|
|
40
|
+
lines.append(f" {group}:")
|
|
41
|
+
for entry in entries:
|
|
42
|
+
lines.append(f" - {entry['xid']} ({entry['load_reason']})")
|
|
43
|
+
lines.append("references:")
|
|
44
|
+
for group, entries in data["references"].items():
|
|
45
|
+
if entries:
|
|
46
|
+
lines.append(f" {group}: {', '.join(entries)}")
|
|
47
|
+
lines.append("required_outputs:")
|
|
48
|
+
for output in data["required_outputs"]:
|
|
49
|
+
lines.append(f" - {output}")
|
|
50
|
+
return "\n".join(lines)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cmd_show_effective_skill(args: argparse.Namespace) -> int:
|
|
54
|
+
registry = build_registry(
|
|
55
|
+
package_manifests=args.package_manifest,
|
|
56
|
+
local_manifest_path=args.local_manifest,
|
|
57
|
+
discover_entry_points=args.enable_entry_point_discovery,
|
|
58
|
+
enabled_package_ids=_enabled_packages_from_args(args),
|
|
59
|
+
)
|
|
60
|
+
bundle = EffectiveSkillResolver(registry).resolve_entry(args.skill_id)
|
|
61
|
+
if args.mode == "tree":
|
|
62
|
+
print(_bundle_tree(bundle))
|
|
63
|
+
elif args.mode == "resolved-json":
|
|
64
|
+
print(json.dumps(bundle.model_dump(mode="json"), ensure_ascii=False, indent=2))
|
|
65
|
+
else:
|
|
66
|
+
raise NotImplementedError("true full materialize is not implemented in the MVP CLI")
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_package_discover(args: argparse.Namespace) -> int:
|
|
71
|
+
rows = [
|
|
72
|
+
{
|
|
73
|
+
"entry_point": package.entry_point_name,
|
|
74
|
+
"package_id": package.package_id,
|
|
75
|
+
"version": package.version,
|
|
76
|
+
"manifest_path": str(package.manifest_path),
|
|
77
|
+
}
|
|
78
|
+
for package in discover_skill_packages()
|
|
79
|
+
]
|
|
80
|
+
if args.json:
|
|
81
|
+
print(json.dumps(rows, ensure_ascii=False, indent=2))
|
|
82
|
+
else:
|
|
83
|
+
for row in rows:
|
|
84
|
+
print(f"{row['package_id']}\t{row['version']}\t{row['manifest_path']}")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_package_list(args: argparse.Namespace) -> int:
|
|
89
|
+
rows = package_list_rows(enabled_package_ids=_enabled_packages_from_args(args))
|
|
90
|
+
if args.json:
|
|
91
|
+
print(json.dumps(rows, ensure_ascii=False, indent=2))
|
|
92
|
+
else:
|
|
93
|
+
for row in rows:
|
|
94
|
+
status = "enabled" if row["enabled"] else "disabled"
|
|
95
|
+
print(f"{row['package_id']}\t{row['version']}\t{status}\t{row['manifest_path']}")
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
100
|
+
parser = argparse.ArgumentParser(prog="xrefkit")
|
|
101
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
102
|
+
|
|
103
|
+
package = subparsers.add_parser("package")
|
|
104
|
+
package_sub = package.add_subparsers(dest="package_command", required=True)
|
|
105
|
+
discover = package_sub.add_parser("discover")
|
|
106
|
+
discover.add_argument("--json", action="store_true")
|
|
107
|
+
discover.set_defaults(func=cmd_package_discover)
|
|
108
|
+
|
|
109
|
+
package_list = package_sub.add_parser("list")
|
|
110
|
+
package_list.add_argument("--json", action="store_true")
|
|
111
|
+
package_list.add_argument("--server-config", help="Path to xrefkit.server.toml with enabled packages")
|
|
112
|
+
package_list.add_argument("--enabled-package", action="append", default=[], help="Package id enabled for resolver use")
|
|
113
|
+
package_list.set_defaults(func=cmd_package_list)
|
|
114
|
+
|
|
115
|
+
show = subparsers.add_parser("show")
|
|
116
|
+
show_sub = show.add_subparsers(dest="show_command", required=True)
|
|
117
|
+
effective = show_sub.add_parser("effective-skill")
|
|
118
|
+
effective.add_argument("skill_id")
|
|
119
|
+
effective.add_argument("--mode", choices=["tree", "resolved-json"], default="tree")
|
|
120
|
+
effective.add_argument("--server-config", help="Path to xrefkit.server.toml with enabled packages")
|
|
121
|
+
_add_workspace_args(effective)
|
|
122
|
+
effective.set_defaults(func=cmd_show_effective_skill)
|
|
123
|
+
|
|
124
|
+
return parser
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
128
|
+
parser = build_parser()
|
|
129
|
+
args = parser.parse_args(argv)
|
|
130
|
+
return args.func(args)
|
xrefkit/workspace.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Workspace assembly helpers for XRefKit v2 MVP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .discovery import enabled_discovered_packages
|
|
8
|
+
from .loaders import load_local_manifest
|
|
9
|
+
from .loaders import load_local_domain_skill, load_package_manifest, load_skill_definition
|
|
10
|
+
from .registry import XRefKitRegistry
|
|
11
|
+
from .models.common import version_satisfies
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CORE_PROTOCOL_VERSION = "2.0.0"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _contained_path(root: Path, relative_path: str, *, label: str) -> Path:
|
|
18
|
+
root = root.resolve()
|
|
19
|
+
candidate = (root / relative_path).resolve()
|
|
20
|
+
try:
|
|
21
|
+
candidate.relative_to(root)
|
|
22
|
+
except ValueError as exc:
|
|
23
|
+
raise ValueError(f"{label} escapes root: {relative_path}") from exc
|
|
24
|
+
return candidate
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _validate_skill_paths(root: Path, skill) -> None:
|
|
28
|
+
_contained_path(root, skill.entry.path, label="skill entry")
|
|
29
|
+
for fragment in skill.required_fragments:
|
|
30
|
+
_contained_path(root, fragment.path, label=f"skill fragment {fragment.id}")
|
|
31
|
+
for branch in skill.branches:
|
|
32
|
+
_contained_path(root, branch.path, label=f"skill branch {branch.id}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_registry(
|
|
36
|
+
*,
|
|
37
|
+
package_manifests: list[str | Path],
|
|
38
|
+
local_manifest_path: str | Path | None = None,
|
|
39
|
+
discover_entry_points: bool = False,
|
|
40
|
+
enabled_package_ids: set[str] | None = None,
|
|
41
|
+
) -> XRefKitRegistry:
|
|
42
|
+
registry = XRefKitRegistry()
|
|
43
|
+
manifest_paths = [Path(path) for path in package_manifests]
|
|
44
|
+
if discover_entry_points:
|
|
45
|
+
enabled = enabled_package_ids or set()
|
|
46
|
+
manifest_paths.extend(candidate.manifest_path for candidate in enabled_discovered_packages(enabled_package_ids=enabled))
|
|
47
|
+
|
|
48
|
+
for manifest_path in manifest_paths:
|
|
49
|
+
manifest_path = Path(manifest_path).resolve()
|
|
50
|
+
manifest = load_package_manifest(manifest_path)
|
|
51
|
+
package_root = manifest_path.parent.resolve()
|
|
52
|
+
if not version_satisfies(CORE_PROTOCOL_VERSION, manifest.requires.xrefkit_core):
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"package {manifest.package_id} requires XRefKit core {manifest.requires.xrefkit_core}, "
|
|
55
|
+
f"current core is {CORE_PROTOCOL_VERSION}"
|
|
56
|
+
)
|
|
57
|
+
for asset_group in (
|
|
58
|
+
manifest.provides.skills,
|
|
59
|
+
manifest.provides.fragments,
|
|
60
|
+
manifest.provides.knowledge,
|
|
61
|
+
manifest.provides.review_axes,
|
|
62
|
+
manifest.provides.schemas,
|
|
63
|
+
manifest.provides.templates,
|
|
64
|
+
):
|
|
65
|
+
for asset in asset_group:
|
|
66
|
+
_contained_path(package_root, asset.path, label=f"package asset {asset.id}")
|
|
67
|
+
registry.add_package(manifest, package_root)
|
|
68
|
+
for provided in manifest.provides.skills:
|
|
69
|
+
skill_path = _contained_path(package_root, provided.path, label=f"package skill {provided.id}")
|
|
70
|
+
skill = load_skill_definition(skill_path)
|
|
71
|
+
_validate_skill_paths(package_root, skill)
|
|
72
|
+
registry.add_package_skill(
|
|
73
|
+
package_id=manifest.package_id,
|
|
74
|
+
skill=skill,
|
|
75
|
+
path=skill_path,
|
|
76
|
+
root=package_root,
|
|
77
|
+
)
|
|
78
|
+
registry.add_package_knowledge_from_manifest(manifest, package_root)
|
|
79
|
+
if local_manifest_path is not None:
|
|
80
|
+
local_manifest_path = Path(local_manifest_path).resolve()
|
|
81
|
+
local_manifest = load_local_manifest(local_manifest_path)
|
|
82
|
+
local_root = local_manifest_path.parent.resolve()
|
|
83
|
+
if not version_satisfies(CORE_PROTOCOL_VERSION, local_manifest.requires.xrefkit_core):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"local manifest requires XRefKit core {local_manifest.requires.xrefkit_core}, "
|
|
86
|
+
f"current core is {CORE_PROTOCOL_VERSION}"
|
|
87
|
+
)
|
|
88
|
+
for asset_group in (
|
|
89
|
+
local_manifest.mounts.knowledge,
|
|
90
|
+
local_manifest.mounts.templates,
|
|
91
|
+
local_manifest.mounts.schemas,
|
|
92
|
+
local_manifest.mounts.review_axes,
|
|
93
|
+
):
|
|
94
|
+
for asset in asset_group:
|
|
95
|
+
_contained_path(local_root, asset.path, label=f"local asset {asset.id}")
|
|
96
|
+
for mount in local_manifest.mounts.skills:
|
|
97
|
+
_contained_path(local_root, mount.path, label="local skill")
|
|
98
|
+
for requirement in local_manifest.requires.skill_packages:
|
|
99
|
+
package = registry.packages.get(requirement.package_id)
|
|
100
|
+
if package is None:
|
|
101
|
+
raise ValueError(f"required package is not loaded: {requirement.package_id}")
|
|
102
|
+
if not version_satisfies(package.manifest.version, requirement.version):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"package {requirement.package_id} version {package.manifest.version} "
|
|
105
|
+
f"does not satisfy local requirement {requirement.version}"
|
|
106
|
+
)
|
|
107
|
+
registry.add_local_manifest_assets(local_root, local_manifest)
|
|
108
|
+
for mount in local_manifest.mounts.skills:
|
|
109
|
+
skill_path = _contained_path(local_root, mount.path, label="local skill")
|
|
110
|
+
skill = load_local_domain_skill(skill_path)
|
|
111
|
+
registry.add_local_skill(
|
|
112
|
+
skill=skill,
|
|
113
|
+
path=skill_path,
|
|
114
|
+
root=local_root,
|
|
115
|
+
local_id=local_manifest.local_id,
|
|
116
|
+
)
|
|
117
|
+
return registry
|