@yottameta/yotta-dev-mcp-plugin 0.1.1 → 0.2.0

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.
@@ -8,7 +8,6 @@ tool explicitly documents a write.
8
8
  """
9
9
 
10
10
  import argparse
11
- import ast
12
11
  import json
13
12
  import math
14
13
  import os
@@ -18,133 +17,27 @@ import subprocess
18
17
  import sys
19
18
  from pathlib import Path
20
19
 
20
+ import dev_contract
21
+ from dev_adapters import run_adapter as _run_adapter_impl
22
+ from dev_architecture import architecture_review as _architecture_review_impl
23
+ from dev_common import (
24
+ ANSI_RE, ERROR_RE, MAX_FILE_BYTES, VERIFY_LEVELS, _iter_files, _json_safe,
25
+ _language, _read_text, _rel,
26
+ _frontmatter_name, _frontmatter_version, source_exts,
27
+ )
28
+ from dev_impact import impact_analysis as _impact_analysis_impl
29
+ from dev_verify import verify_change as _verify_change_impl
30
+ from dev_selftest import self_test
31
+ from dev_model import _repo_map_js, _repo_map_python, system_model as _system_model_impl
21
32
  from dev_rules import REVIEW_RULES
22
33
 
23
- VERSION = "0.1.1"
34
+ VERSION = "0.2.0"
24
35
 
25
- IGNORE_DIRS = {
26
- ".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv",
27
- "dist", "build", ".next", ".nuxt", ".cache", ".tmp", ".tmp2",
28
- }
29
- SOURCE_EXTS = {
30
- ".py", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".sh", ".ps1",
31
- ".go", ".rs", ".java", ".kt", ".kts", ".rb", ".php",
32
- }
33
- TEXT_EXTS = SOURCE_EXTS | {".json", ".md", ".txt", ".yml", ".yaml", ".toml", ".ini", ".cfg"}
34
- MAX_FILE_BYTES = 2 * 1024 * 1024
35
-
36
- ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
37
- ERROR_RE = re.compile(
38
- r"(?i)(traceback|exception|\berror\b|\bfailed\b|\bfail\b|fatal|panic|"
39
- r"assertion|npm err!|\berr\b|\bwarn(?:ing)?\b)"
40
- )
41
36
 
42
37
 
43
- def _json_safe(value):
44
- if isinstance(value, Path):
45
- return str(value)
46
- if isinstance(value, dict):
47
- return {str(k): _json_safe(v) for k, v in value.items()}
48
- if isinstance(value, list):
49
- return [_json_safe(v) for v in value]
50
- return value
51
38
 
52
39
 
53
- def _read_text(path):
54
- path = Path(path)
55
- if path.stat().st_size > MAX_FILE_BYTES:
56
- raise ValueError("文件超过大小上限: %s" % path)
57
- data = path.read_bytes()
58
- if b"\x00" in data[:4096]:
59
- raise ValueError("二进制文件不参与文本扫描: %s" % path)
60
- return data.decode("utf-8", errors="replace")
61
-
62
-
63
- def _iter_files(root, extensions=None, max_files=5000, all_files=False):
64
- root = Path(root)
65
- extensions = set(extensions or TEXT_EXTS)
66
- count = 0
67
- for current, dirs, files in os.walk(str(root)):
68
- dirs[:] = sorted(d for d in dirs if d not in IGNORE_DIRS)
69
- for name in sorted(files):
70
- path = Path(current) / name
71
- if path.is_symlink():
72
- continue
73
- if not all_files and extensions and path.suffix.lower() not in extensions:
74
- continue
75
- if path.stat().st_size > MAX_FILE_BYTES:
76
- continue
77
- yield path
78
- count += 1
79
- if count >= max_files:
80
- return
81
-
82
40
 
83
- def _rel(root, path):
84
- try:
85
- return str(Path(path).resolve().relative_to(Path(root).resolve())).replace("\\", "/")
86
- except ValueError:
87
- return str(Path(path)).replace("\\", "/")
88
-
89
-
90
- def _language(path):
91
- ext = Path(path).suffix.lower()
92
- if ext == ".py":
93
- return "python"
94
- if ext in (".js", ".jsx", ".mjs", ".cjs"):
95
- return "javascript"
96
- if ext in (".ts", ".tsx"):
97
- return "typescript"
98
- if ext in (".sh", ".ps1"):
99
- return "shell"
100
- return ext.lstrip(".") or "text"
101
-
102
-
103
- def _resolve_python_import(source_rel, node):
104
- source = Path(source_rel)
105
- if isinstance(node, ast.Import):
106
- return [alias.name for alias in node.names]
107
- if not isinstance(node, ast.ImportFrom):
108
- return []
109
- if node.level:
110
- parts = list(source.with_suffix("").parts[:-1])
111
- if node.level > 1:
112
- parts = parts[:-(node.level - 1)] if len(parts) >= node.level - 1 else []
113
- prefix = "/".join(parts)
114
- module = node.module or ""
115
- target = (prefix + "/" + module.replace(".", "/")).strip("/")
116
- return [target + ".py" if target else source_rel]
117
- if node.module:
118
- return [node.module]
119
- return []
120
-
121
-
122
- def _repo_map_python(path, rel):
123
- text = _read_text(path)
124
- imports = []
125
- try:
126
- tree = ast.parse(text)
127
- except SyntaxError:
128
- return imports
129
- for node in ast.walk(tree):
130
- if isinstance(node, (ast.Import, ast.ImportFrom)):
131
- for target in _resolve_python_import(rel, node):
132
- imports.append({"source": rel, "target": target, "line": getattr(node, "lineno", 1)})
133
- return imports
134
-
135
-
136
- def _repo_map_js(path, rel):
137
- text = _read_text(path)
138
- imports = []
139
- patterns = [
140
- re.compile(r"""(?:from\s+|import\s*\()\s*['"]([^'"]+)['"]"""),
141
- re.compile(r"""require\s*\(\s*['"]([^'"]+)['"]\s*\)"""),
142
- ]
143
- for lineno, line in enumerate(text.splitlines(), 1):
144
- for pattern in patterns:
145
- for match in pattern.finditer(line):
146
- imports.append({"source": rel, "target": match.group(1), "line": lineno})
147
- return imports
148
41
 
149
42
 
150
43
  def repo_map(path, max_files=2000):
@@ -191,10 +84,6 @@ def repo_map(path, max_files=2000):
191
84
  }
192
85
 
193
86
 
194
- def source_exts():
195
- return set(SOURCE_EXTS)
196
-
197
-
198
87
  def _classify_match(line, query):
199
88
  escaped = re.escape(query)
200
89
  if re.search(r"^\s*(?:async\s+)?def\s+%s\b" % escaped, line):
@@ -384,16 +273,6 @@ def review_diff(diff_text=None, path=None, base=None, max_findings=200):
384
273
  return {"files": files, "findings": findings[:max_findings], "truncated": truncated}
385
274
 
386
275
 
387
- def _frontmatter_version(text):
388
- match = re.search(r"(?m)^version:\s*[\"']?([^\"'\r\n]+)", text)
389
- return match.group(1).strip() if match else None
390
-
391
-
392
- def _frontmatter_name(text):
393
- match = re.search(r"(?m)^name:\s*[\"']?([^\"'\r\n]+)", text)
394
- return match.group(1).strip() if match else None
395
-
396
-
397
276
  def _default_skill_dirs():
398
277
  home = Path.home()
399
278
  candidates = [
@@ -956,9 +835,165 @@ def workflow_state(root, action="read", date=None, text=None, file=None, apply=F
956
835
  }
957
836
 
958
837
 
838
+
839
+
840
+
841
+
842
+
843
+
844
+
845
+
846
+
847
+
848
+
849
+
850
+
851
+
852
+
853
+
854
+
855
+
856
+
857
+
858
+
859
+
860
+
861
+
862
+
863
+
864
+
865
+
866
+
867
+
868
+
869
+
870
+
871
+
872
+
873
+
874
+
875
+
876
+
877
+
878
+
879
+
880
+
881
+
882
+
883
+
884
+
885
+
886
+
887
+
888
+
889
+
890
+
891
+
892
+
893
+
894
+
895
+
896
+
897
+
898
+
899
+
900
+
901
+
902
+
903
+
904
+
905
+
906
+
907
+
908
+
909
+
910
+
911
+
912
+
913
+
914
+
915
+
916
+
917
+
918
+
919
+
920
+
921
+
922
+
923
+
924
+
925
+
926
+
927
+
928
+
929
+
930
+
931
+
932
+
933
+
934
+
935
+
936
+
937
+
938
+
939
+
940
+ def system_model(path, max_files=2000, contract_file=None):
941
+ """Build the deterministic system model through the stable engine facade."""
942
+ return _system_model_impl(
943
+ path, max_files=max_files, contract_file=contract_file,
944
+ )
945
+
946
+
947
+ def architecture_review(path, max_files=2000, contract_file=None):
948
+ """Review architecture through the stable engine facade."""
949
+ return _architecture_review_impl(
950
+ path, max_files=max_files, contract_file=contract_file,
951
+ )
952
+
953
+
954
+ def impact_analysis(path, changed_files=None, diff=None, symbols=None, depth=3,
955
+ max_files=2000, contract_file=None):
956
+ """Build the change impact cone through the stable engine facade."""
957
+ return _impact_analysis_impl(
958
+ path, changed_files=changed_files, diff=diff, symbols=symbols,
959
+ depth=depth, max_files=max_files, contract_file=contract_file,
960
+ )
961
+
962
+
963
+ def verify_change(path, changed_files=None, diff=None, symbols=None, depth=3,
964
+ levels=None, allow_execute=False, timeout=120,
965
+ max_files=2000, contract_file=None, policy_file=None):
966
+ """Run the verification ladder through the stable engine facade."""
967
+ return _verify_change_impl(
968
+ path, changed_files=changed_files, diff=diff, symbols=symbols,
969
+ depth=depth, levels=levels, allow_execute=allow_execute, timeout=timeout,
970
+ max_files=max_files, contract_file=contract_file, policy_file=policy_file,
971
+ )
972
+
973
+
974
+ def run_adapter(path, action="list", adapter=None, allow_execute=False, timeout=120,
975
+ max_chars=120000, token_budget=None, target="."):
976
+ """Probe or explicitly run one optional external adapter.
977
+
978
+ The implementation lives in dev_adapters.py; this facade keeps the public
979
+ dispatch signature and fail-closed default visible to self_test.
980
+ """
981
+ return _run_adapter_impl(
982
+ path, action=action, adapter=adapter, allow_execute=allow_execute,
983
+ timeout=timeout, max_chars=max_chars, token_budget=token_budget,
984
+ target=target,
985
+ )
986
+
987
+
959
988
  def dispatch(name, arguments):
960
989
  handlers = {
961
990
  "repo_map": repo_map,
991
+ "system_model": system_model,
992
+ "architecture_review": architecture_review,
993
+ "impact_analysis": impact_analysis,
994
+ "verify_change": verify_change,
995
+ "self_test": self_test,
996
+ "run_adapter": run_adapter,
962
997
  "find_code": find_code,
963
998
  "compress_output": compress_output,
964
999
  "review_code": review_code,
@@ -982,6 +1017,62 @@ def main():
982
1017
  sub = parser.add_subparsers(dest="command")
983
1018
  repo = sub.add_parser("repo-map")
984
1019
  repo.add_argument("path")
1020
+ model = sub.add_parser("system-model")
1021
+ model.add_argument("path")
1022
+ model.add_argument("--contract", help="contract path relative to the repository root")
1023
+ review = sub.add_parser("architecture-review")
1024
+ review.add_argument("path")
1025
+ review.add_argument("--contract", help="contract path relative to the repository root")
1026
+ impact = sub.add_parser("impact-analysis")
1027
+ impact.add_argument("path")
1028
+ impact.add_argument("--changed", action="append",
1029
+ help="repository-relative changed file (repeatable)")
1030
+ impact.add_argument("--diff-file", help="read a unified diff from this file")
1031
+ impact.add_argument("--symbol", action="append",
1032
+ help="target symbol whose definition site is the change (repeatable)")
1033
+ impact.add_argument("--depth", type=int, default=3,
1034
+ help="reverse-dependency depth, 1-10 (default 3)")
1035
+ impact.add_argument("--contract", help="contract path relative to the repository root")
1036
+ verify = sub.add_parser("verify-change")
1037
+ verify.add_argument("path")
1038
+ verify.add_argument("--changed", action="append",
1039
+ help="repository-relative changed file (repeatable)")
1040
+ verify.add_argument("--diff-file", help="read a unified diff from this file")
1041
+ verify.add_argument("--symbol", action="append",
1042
+ help="target symbol whose definition site is the change (repeatable)")
1043
+ verify.add_argument("--level", action="append", choices=VERIFY_LEVELS,
1044
+ help="additional verification level (repeatable)")
1045
+ verify.add_argument("--depth", type=int, default=3,
1046
+ help="reverse-dependency depth, 1-10 (default 3)")
1047
+ verify.add_argument("--contract", help="contract path relative to the repository root")
1048
+ verify.add_argument("--policy-file", help="verification policy path relative to the root")
1049
+ verify.add_argument("--allow-execute", action="store_true",
1050
+ help="run whitelisted L2-L4 policy checks")
1051
+ verify.add_argument("--timeout", type=int, default=120,
1052
+ help="upper bound for each check in seconds")
1053
+ self_test_parser = sub.add_parser("self-test")
1054
+ self_test_parser.add_argument("path")
1055
+ self_test_parser.add_argument("--mode", choices=("auto", "source", "installed"),
1056
+ default="auto")
1057
+ self_test_parser.add_argument("--allow-execute", action="store_true",
1058
+ help="run the target test suite")
1059
+ self_test_parser.add_argument("--timeout", type=int, default=120,
1060
+ help="test timeout in seconds")
1061
+ adapter = sub.add_parser("adapter")
1062
+ adapter.add_argument("path")
1063
+ adapter.add_argument("--action", choices=("list", "run"), default="list")
1064
+ adapter.add_argument("--adapter",
1065
+ choices=("import-linter", "dependency-cruiser", "repomix"))
1066
+ adapter.add_argument("--allow-execute", action="store_true",
1067
+ help="run the selected adapter; required for action=run")
1068
+ adapter.add_argument("--timeout", type=int, default=120,
1069
+ help="adapter timeout in seconds")
1070
+ adapter.add_argument("--max-chars", type=int, default=120000,
1071
+ help="Repomix output cap")
1072
+ adapter.add_argument("--token-budget", type=int,
1073
+ help="optional Repomix token budget")
1074
+ adapter.add_argument("--target", default=".",
1075
+ help="repository-relative adapter target, default .")
985
1076
  find = sub.add_parser("find-code")
986
1077
  find.add_argument("path")
987
1078
  find.add_argument("query")
@@ -995,6 +1086,39 @@ def main():
995
1086
  args = parser.parse_args()
996
1087
  if args.command == "repo-map":
997
1088
  result = repo_map(args.path)
1089
+ elif args.command == "system-model":
1090
+ result = system_model(args.path, contract_file=args.contract)
1091
+ elif args.command == "architecture-review":
1092
+ result = architecture_review(args.path, contract_file=args.contract)
1093
+ elif args.command == "impact-analysis":
1094
+ diff_text = None
1095
+ if args.diff_file:
1096
+ diff_text = Path(args.diff_file).read_text(encoding="utf-8")
1097
+ result = impact_analysis(args.path, changed_files=args.changed, diff=diff_text,
1098
+ symbols=args.symbol, depth=args.depth,
1099
+ contract_file=args.contract)
1100
+ elif args.command == "verify-change":
1101
+ diff_text = None
1102
+ if args.diff_file:
1103
+ diff_text = Path(args.diff_file).read_text(encoding="utf-8")
1104
+ result = verify_change(
1105
+ args.path, changed_files=args.changed, diff=diff_text,
1106
+ symbols=args.symbol, depth=args.depth, levels=args.level,
1107
+ allow_execute=args.allow_execute, timeout=args.timeout,
1108
+ contract_file=args.contract, policy_file=args.policy_file,
1109
+ )
1110
+ elif args.command == "self-test":
1111
+ result = self_test(
1112
+ args.path, mode=args.mode, allow_execute=args.allow_execute,
1113
+ timeout=args.timeout,
1114
+ )
1115
+ elif args.command == "adapter":
1116
+ result = run_adapter(
1117
+ args.path, action=args.action, adapter=args.adapter,
1118
+ allow_execute=args.allow_execute, timeout=args.timeout,
1119
+ max_chars=args.max_chars, token_budget=args.token_budget,
1120
+ target=args.target,
1121
+ )
998
1122
  elif args.command == "find-code":
999
1123
  result = find_code(args.path, args.query)
1000
1124
  elif args.command == "compress-output":