@yottameta/yotta-dev-mcp-plugin 0.0.0 → 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.
- package/.agents/plugins/marketplace.json +25 -0
- package/.claude-plugin/marketplace.json +25 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/mcp.json +12 -0
- package/package.json +13 -10
- package/plugin.json +20 -0
- package/skills/yotta-dev-mcp/LICENSE +21 -0
- package/skills/yotta-dev-mcp/NOTICE +7 -0
- package/skills/yotta-dev-mcp/SKILL.md +81 -0
- package/skills/yotta-dev-mcp/assets/banner.png +0 -0
- package/skills/yotta-dev-mcp/bin/yotta-dev-mcp.js +57 -0
- package/skills/yotta-dev-mcp/references/adapters.md +69 -0
- package/skills/yotta-dev-mcp/references/architecture-contract.md +256 -0
- package/skills/yotta-dev-mcp/references/tools.md +257 -0
- package/skills/yotta-dev-mcp/scripts/dev_adapters.py +609 -0
- package/skills/yotta-dev-mcp/scripts/dev_architecture.py +379 -0
- package/skills/yotta-dev-mcp/scripts/dev_common.py +144 -0
- package/skills/yotta-dev-mcp/scripts/dev_contract.py +830 -0
- package/skills/yotta-dev-mcp/scripts/dev_engine.py +1138 -0
- package/skills/yotta-dev-mcp/scripts/dev_impact.py +556 -0
- package/skills/yotta-dev-mcp/scripts/dev_model.py +435 -0
- package/skills/yotta-dev-mcp/scripts/dev_rules.py +25 -0
- package/skills/yotta-dev-mcp/scripts/dev_selftest.py +534 -0
- package/skills/yotta-dev-mcp/scripts/dev_verify.py +450 -0
- package/skills/yotta-dev-mcp/scripts/yotta_dev_mcp.py +659 -0
- package/skills/yotta-dev-mcp/server.json +20 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Architecture contract review for yotta-dev-mcp."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import dev_contract
|
|
9
|
+
from dev_common import EVIDENCE_LIMIT, UNKNOWN_LIMIT, _read_text
|
|
10
|
+
from dev_model import (
|
|
11
|
+
_decisive_edges, _edge_evidence, _module_layers, _risk_weight,
|
|
12
|
+
system_model,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _glob_literal_prefix(pattern):
|
|
17
|
+
text = str(pattern or "")
|
|
18
|
+
for marker in ("*", "?"):
|
|
19
|
+
index = text.find(marker)
|
|
20
|
+
if index >= 0:
|
|
21
|
+
text = text[:index]
|
|
22
|
+
return text.rstrip("/")
|
|
23
|
+
|
|
24
|
+
def _review_rules(contract, model, unknown_sink, violation_sink):
|
|
25
|
+
module_layers = _module_layers(model)
|
|
26
|
+
edges = sorted(_decisive_edges(model),
|
|
27
|
+
key=lambda item: (item["source"], item["line"], item["target"]))
|
|
28
|
+
checked = []
|
|
29
|
+
for rule in contract["rules"]:
|
|
30
|
+
severity = rule.get("severity") or dev_contract.RULE_DEFAULT_SEVERITY
|
|
31
|
+
violations = 0
|
|
32
|
+
undecided = 0
|
|
33
|
+
decisive = 0
|
|
34
|
+
seen_targets = set()
|
|
35
|
+
for edge in edges:
|
|
36
|
+
if module_layers.get(edge["source"]) != rule["from"]:
|
|
37
|
+
continue
|
|
38
|
+
target_layer = module_layers.get(edge["target"])
|
|
39
|
+
if target_layer is None:
|
|
40
|
+
if edge["kind"] == "internal" and edge["target"] not in seen_targets:
|
|
41
|
+
seen_targets.add(edge["target"])
|
|
42
|
+
undecided += 1
|
|
43
|
+
unknown_sink.append({
|
|
44
|
+
"kind": "rule-target-unassigned",
|
|
45
|
+
"id": "%s -> %s" % (rule["id"], edge["target"]),
|
|
46
|
+
"rule": rule["id"],
|
|
47
|
+
"target": edge["target"],
|
|
48
|
+
"detail": "the target module has no layer, so the rule cannot be decided",
|
|
49
|
+
"next_step": "assign the module to a layer or narrow the rule",
|
|
50
|
+
})
|
|
51
|
+
continue
|
|
52
|
+
decisive += 1
|
|
53
|
+
if rule["type"] == "forbid-dependency":
|
|
54
|
+
broke = target_layer == rule["to"]
|
|
55
|
+
message = rule.get("claim") or (
|
|
56
|
+
"%s must not import %s" % (rule["from"], rule["to"]))
|
|
57
|
+
code = "rule-forbid-dependency"
|
|
58
|
+
else:
|
|
59
|
+
broke = target_layer not in (rule["to"] or [])
|
|
60
|
+
message = rule.get("claim") or (
|
|
61
|
+
"%s may only import %s" % (rule["from"], ", ".join(rule["to"] or [])))
|
|
62
|
+
code = "rule-allow-dependency"
|
|
63
|
+
if not broke:
|
|
64
|
+
continue
|
|
65
|
+
violations += 1
|
|
66
|
+
violation_sink.append({
|
|
67
|
+
"code": code,
|
|
68
|
+
"rule": rule["id"],
|
|
69
|
+
"severity": severity,
|
|
70
|
+
"message": message,
|
|
71
|
+
"from_layer": rule["from"],
|
|
72
|
+
"to_layer": target_layer,
|
|
73
|
+
"evidence": [_edge_evidence(
|
|
74
|
+
edge,
|
|
75
|
+
"imports %s (layer %s), which breaks %s"
|
|
76
|
+
% (edge["target"], target_layer, rule["id"]),
|
|
77
|
+
)],
|
|
78
|
+
})
|
|
79
|
+
if violations and severity in dev_contract.BLOCKING_SEVERITIES:
|
|
80
|
+
status = "FAIL"
|
|
81
|
+
elif violations:
|
|
82
|
+
status = "WARN"
|
|
83
|
+
elif undecided:
|
|
84
|
+
status = "UNKNOWN"
|
|
85
|
+
else:
|
|
86
|
+
status = "PASS"
|
|
87
|
+
checked.append({
|
|
88
|
+
"id": rule["id"],
|
|
89
|
+
"type": rule["type"],
|
|
90
|
+
"from": rule["from"],
|
|
91
|
+
"to": rule["to"],
|
|
92
|
+
"severity": severity,
|
|
93
|
+
"claim": rule.get("claim"),
|
|
94
|
+
"status": status,
|
|
95
|
+
"decided_imports": decisive,
|
|
96
|
+
"violations": violations,
|
|
97
|
+
"undecided_imports": undecided,
|
|
98
|
+
})
|
|
99
|
+
return checked
|
|
100
|
+
|
|
101
|
+
def _review_boundaries(contract, model, unknown_sink, violation_sink):
|
|
102
|
+
module_layers = _module_layers(model)
|
|
103
|
+
edges = sorted(_decisive_edges(model),
|
|
104
|
+
key=lambda item: (item["source"], item["line"], item["target"]))
|
|
105
|
+
checked = []
|
|
106
|
+
for boundary in contract["boundaries"]:
|
|
107
|
+
paths = boundary["paths"] or []
|
|
108
|
+
protected = sorted(item["id"] for item in model["modules"]
|
|
109
|
+
if dev_contract.match_any(item["id"], paths))
|
|
110
|
+
visibility = boundary.get("visibility") or "internal"
|
|
111
|
+
severity = "high" if visibility == "private" else "medium"
|
|
112
|
+
violations = 0
|
|
113
|
+
undecided = 0
|
|
114
|
+
consumers = 0
|
|
115
|
+
seen_importers = set()
|
|
116
|
+
if not protected:
|
|
117
|
+
unknown_sink.append({
|
|
118
|
+
"kind": "boundary-no-modules",
|
|
119
|
+
"id": boundary["id"],
|
|
120
|
+
"detail": "no module matches the boundary globs",
|
|
121
|
+
"next_step": "point the boundary at existing modules or drop it",
|
|
122
|
+
})
|
|
123
|
+
protected_set = set(protected)
|
|
124
|
+
for edge in edges:
|
|
125
|
+
if edge["target"] not in protected_set:
|
|
126
|
+
continue
|
|
127
|
+
consumers += 1
|
|
128
|
+
importer = edge["source"]
|
|
129
|
+
if dev_contract.match_any(importer, paths) or visibility == "public":
|
|
130
|
+
continue
|
|
131
|
+
importer_layer = module_layers.get(importer)
|
|
132
|
+
if importer_layer is None:
|
|
133
|
+
if importer not in seen_importers:
|
|
134
|
+
seen_importers.add(importer)
|
|
135
|
+
undecided += 1
|
|
136
|
+
unknown_sink.append({
|
|
137
|
+
"kind": "boundary-importer-unassigned",
|
|
138
|
+
"id": "%s <- %s" % (boundary["id"], importer),
|
|
139
|
+
"boundary": boundary["id"],
|
|
140
|
+
"target": importer,
|
|
141
|
+
"detail": "the importer has no layer, so boundary visibility is undecided",
|
|
142
|
+
"next_step": "assign the importer to a layer",
|
|
143
|
+
})
|
|
144
|
+
continue
|
|
145
|
+
if visibility == "internal" and importer_layer == boundary["layer"]:
|
|
146
|
+
continue
|
|
147
|
+
violations += 1
|
|
148
|
+
violation_sink.append({
|
|
149
|
+
"code": "boundary-visibility",
|
|
150
|
+
"rule": boundary["id"],
|
|
151
|
+
"severity": severity,
|
|
152
|
+
"message": "module outside the %s boundary imports it (visibility %s)"
|
|
153
|
+
% (boundary["id"], visibility),
|
|
154
|
+
"boundary": boundary["id"],
|
|
155
|
+
"layer": boundary["layer"],
|
|
156
|
+
"visibility": visibility,
|
|
157
|
+
"evidence": [_edge_evidence(
|
|
158
|
+
edge,
|
|
159
|
+
"imports %s, protected by the %s boundary (%s)"
|
|
160
|
+
% (edge["target"], boundary["id"], visibility),
|
|
161
|
+
)],
|
|
162
|
+
})
|
|
163
|
+
if violations and severity in dev_contract.BLOCKING_SEVERITIES:
|
|
164
|
+
status = "FAIL"
|
|
165
|
+
elif violations:
|
|
166
|
+
status = "WARN"
|
|
167
|
+
elif undecided or not protected:
|
|
168
|
+
status = "UNKNOWN"
|
|
169
|
+
else:
|
|
170
|
+
status = "PASS"
|
|
171
|
+
checked.append({
|
|
172
|
+
"id": boundary["id"],
|
|
173
|
+
"layer": boundary["layer"],
|
|
174
|
+
"visibility": visibility,
|
|
175
|
+
"paths": list(paths),
|
|
176
|
+
"status": status,
|
|
177
|
+
"protected_modules": protected,
|
|
178
|
+
"imports_of_protected_modules": consumers,
|
|
179
|
+
"violations": violations,
|
|
180
|
+
"undecided_imports": undecided,
|
|
181
|
+
})
|
|
182
|
+
return checked
|
|
183
|
+
|
|
184
|
+
def _review_data_ownership(contract, model, root, violation_sink):
|
|
185
|
+
module_layers = _module_layers(model)
|
|
186
|
+
stores = contract["data_ownership"]
|
|
187
|
+
prefixes = {}
|
|
188
|
+
for store in stores:
|
|
189
|
+
candidates = [_glob_literal_prefix(item) for item in store["paths"] or []]
|
|
190
|
+
prefixes[store["store"]] = sorted(item for item in candidates if len(item) >= 4)
|
|
191
|
+
needed = sorted({item for values in prefixes.values() for item in values})
|
|
192
|
+
texts = {}
|
|
193
|
+
if needed:
|
|
194
|
+
for module in model["modules"]:
|
|
195
|
+
if module.get("layer") is None:
|
|
196
|
+
continue
|
|
197
|
+
try:
|
|
198
|
+
texts[module["id"]] = _read_text(root / module["id"])
|
|
199
|
+
except (OSError, ValueError):
|
|
200
|
+
continue
|
|
201
|
+
checked = []
|
|
202
|
+
for store in stores:
|
|
203
|
+
owner = store["owner"]
|
|
204
|
+
paths = store["paths"] or []
|
|
205
|
+
violations = 0
|
|
206
|
+
mismatched = sorted(item["id"] for item in model["modules"]
|
|
207
|
+
if dev_contract.match_any(item["id"], paths)
|
|
208
|
+
and item.get("layer") != owner)
|
|
209
|
+
for rel in mismatched:
|
|
210
|
+
violations += 1
|
|
211
|
+
violation_sink.append({
|
|
212
|
+
"code": "data-ownership-mismatch",
|
|
213
|
+
"rule": store["store"],
|
|
214
|
+
"severity": "medium",
|
|
215
|
+
"message": "module inside the store paths belongs to layer %s, not the owner %s"
|
|
216
|
+
% (module_layers.get(rel), owner),
|
|
217
|
+
"store": store["store"],
|
|
218
|
+
"owner": owner,
|
|
219
|
+
"evidence": [{"path": rel, "line": 1, "detail":
|
|
220
|
+
"matches the declared paths of store %s" % store["store"]}],
|
|
221
|
+
})
|
|
222
|
+
references = 0
|
|
223
|
+
store_prefixes = prefixes.get(store["store"]) or []
|
|
224
|
+
if store_prefixes:
|
|
225
|
+
for rel in sorted(texts):
|
|
226
|
+
if module_layers.get(rel) == owner:
|
|
227
|
+
continue
|
|
228
|
+
for lineno, line in enumerate(texts[rel].splitlines(), 1):
|
|
229
|
+
if any(prefix in line for prefix in store_prefixes):
|
|
230
|
+
references += 1
|
|
231
|
+
violations += 1
|
|
232
|
+
violation_sink.append({
|
|
233
|
+
"code": "data-store-access-outside-owner",
|
|
234
|
+
"rule": store["store"],
|
|
235
|
+
"severity": "medium",
|
|
236
|
+
"message": "layer %s references the store owned by %s"
|
|
237
|
+
% (module_layers.get(rel), owner),
|
|
238
|
+
"store": store["store"],
|
|
239
|
+
"owner": owner,
|
|
240
|
+
"evidence": [{
|
|
241
|
+
"path": rel,
|
|
242
|
+
"line": lineno,
|
|
243
|
+
"detail": "references the %s store path" % store["store"],
|
|
244
|
+
"snippet": line.strip()[:200],
|
|
245
|
+
}],
|
|
246
|
+
})
|
|
247
|
+
break
|
|
248
|
+
owner_modules = sorted(item["id"] for item in model["modules"]
|
|
249
|
+
if item.get("layer") == owner)
|
|
250
|
+
checked.append({
|
|
251
|
+
"store": store["store"],
|
|
252
|
+
"owner": owner,
|
|
253
|
+
"kind": store.get("kind"),
|
|
254
|
+
"paths": list(paths),
|
|
255
|
+
"status": "WARN" if violations else "PASS",
|
|
256
|
+
"owner_modules": len(owner_modules),
|
|
257
|
+
"mismatched_modules": mismatched,
|
|
258
|
+
"references_outside_owner": references,
|
|
259
|
+
})
|
|
260
|
+
return checked
|
|
261
|
+
|
|
262
|
+
def _review_invariants(contract, unverified_sink):
|
|
263
|
+
checked = []
|
|
264
|
+
for invariant in contract["invariants"]:
|
|
265
|
+
check = invariant.get("check") or "manual"
|
|
266
|
+
if check == "static":
|
|
267
|
+
reason = "no built-in evaluator covers this claim yet"
|
|
268
|
+
elif check == "command":
|
|
269
|
+
reason = "architecture_review never executes commands"
|
|
270
|
+
else:
|
|
271
|
+
reason = "human review is required"
|
|
272
|
+
unverified_sink.append({
|
|
273
|
+
"claim": invariant["claim"],
|
|
274
|
+
"level": "L1",
|
|
275
|
+
"status": "UNVERIFIED",
|
|
276
|
+
"reason": reason,
|
|
277
|
+
"invariant": invariant["id"],
|
|
278
|
+
"check": check,
|
|
279
|
+
"severity": invariant.get("severity") or dev_contract.RULE_DEFAULT_SEVERITY,
|
|
280
|
+
"paths": list(invariant.get("paths") or []),
|
|
281
|
+
})
|
|
282
|
+
checked.append({
|
|
283
|
+
"id": invariant["id"],
|
|
284
|
+
"claim": invariant["claim"],
|
|
285
|
+
"check": check,
|
|
286
|
+
"severity": invariant.get("severity") or dev_contract.RULE_DEFAULT_SEVERITY,
|
|
287
|
+
"paths": list(invariant.get("paths") or []),
|
|
288
|
+
"status": "UNVERIFIED",
|
|
289
|
+
"reason": reason,
|
|
290
|
+
})
|
|
291
|
+
return checked
|
|
292
|
+
|
|
293
|
+
def _architecture_review_core(model_result, contract_result, root):
|
|
294
|
+
"""Shared review core: evaluate the contract against one system model."""
|
|
295
|
+
contract = contract_result.get("contract") if contract_result.get("ok") else None
|
|
296
|
+
violations = []
|
|
297
|
+
unknowns = [dict(item) for item in model_result["unknowns"]]
|
|
298
|
+
unverified = []
|
|
299
|
+
evidence = []
|
|
300
|
+
checked = {"rules": [], "boundaries": [], "data_stores": [], "invariants": []}
|
|
301
|
+
if contract:
|
|
302
|
+
checked["rules"] = _review_rules(contract, model_result["model"], unknowns, violations)
|
|
303
|
+
checked["boundaries"] = _review_boundaries(
|
|
304
|
+
contract, model_result["model"], unknowns, violations)
|
|
305
|
+
checked["data_stores"] = _review_data_ownership(
|
|
306
|
+
contract, model_result["model"], root, violations)
|
|
307
|
+
checked["invariants"] = _review_invariants(contract, unverified)
|
|
308
|
+
if model_result["truncated"]:
|
|
309
|
+
unknowns.append({
|
|
310
|
+
"kind": "model-truncated",
|
|
311
|
+
"id": model_result["root"],
|
|
312
|
+
"detail": "the file limit cut the scan short; some modules were not reviewed",
|
|
313
|
+
"next_step": "raise max_files and rerun the review",
|
|
314
|
+
})
|
|
315
|
+
blocking = [item for item in violations
|
|
316
|
+
if item["severity"] in dev_contract.BLOCKING_SEVERITIES]
|
|
317
|
+
advisory = [item for item in violations
|
|
318
|
+
if item["severity"] not in dev_contract.BLOCKING_SEVERITIES]
|
|
319
|
+
contract_blocking = [item for item in model_result["contract"]["findings"]
|
|
320
|
+
if item["severity"] in dev_contract.BLOCKING_SEVERITIES]
|
|
321
|
+
for item in violations:
|
|
322
|
+
for entry in item["evidence"]:
|
|
323
|
+
evidence.append({
|
|
324
|
+
"path": entry["path"],
|
|
325
|
+
"line": entry.get("line"),
|
|
326
|
+
"detail": "%s: %s" % (item["code"], entry["detail"]),
|
|
327
|
+
})
|
|
328
|
+
for item in model_result["contract"]["findings"]:
|
|
329
|
+
evidence.append({
|
|
330
|
+
"path": model_result["contract"]["path"],
|
|
331
|
+
"pointer": item["pointer"],
|
|
332
|
+
"detail": "%s: %s" % (item["severity"], item["message"]),
|
|
333
|
+
})
|
|
334
|
+
evidence.sort(key=lambda item: (item["path"], item.get("line") or 0, item["detail"]))
|
|
335
|
+
if len(evidence) > EVIDENCE_LIMIT:
|
|
336
|
+
evidence = evidence[:EVIDENCE_LIMIT]
|
|
337
|
+
if contract_blocking or blocking:
|
|
338
|
+
status = "FAIL"
|
|
339
|
+
elif not model_result["contract"]["present"] or unknowns:
|
|
340
|
+
status = "UNKNOWN"
|
|
341
|
+
else:
|
|
342
|
+
status = "PASS"
|
|
343
|
+
violations.sort(key=lambda item: (item["code"], item["rule"], item["evidence"][0]["path"]))
|
|
344
|
+
unknowns.sort(key=lambda item: (item["kind"], item["id"]))
|
|
345
|
+
return {
|
|
346
|
+
"status": status,
|
|
347
|
+
"checked": checked,
|
|
348
|
+
"violations": violations,
|
|
349
|
+
"blocking_findings": len(blocking) + len(contract_blocking),
|
|
350
|
+
"advisory_findings": len(advisory),
|
|
351
|
+
"unknowns": unknowns,
|
|
352
|
+
"unverified_claims": unverified,
|
|
353
|
+
"evidence": evidence,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
def architecture_review(path, max_files=2000, contract_file=None):
|
|
357
|
+
"""Review a repository against `.yotta/architecture.json`. Read-only."""
|
|
358
|
+
root = Path(path)
|
|
359
|
+
if not root.exists():
|
|
360
|
+
raise ValueError("路径不存在: %s" % path)
|
|
361
|
+
if not root.is_dir():
|
|
362
|
+
raise ValueError("architecture_review 需要目录: %s" % path)
|
|
363
|
+
model_result = system_model(str(root), max_files=max_files, contract_file=contract_file)
|
|
364
|
+
contract_result = dev_contract.load_contract(root, contract_file=contract_file)
|
|
365
|
+
core = _architecture_review_core(model_result, contract_result, root)
|
|
366
|
+
return {
|
|
367
|
+
"status": core["status"],
|
|
368
|
+
"root": model_result["root"],
|
|
369
|
+
"contract": model_result["contract"],
|
|
370
|
+
"checked": core["checked"],
|
|
371
|
+
"violations": core["violations"],
|
|
372
|
+
"blocking_findings": core["blocking_findings"],
|
|
373
|
+
"advisory_findings": core["advisory_findings"],
|
|
374
|
+
"unknowns": core["unknowns"],
|
|
375
|
+
"unverified_claims": core["unverified_claims"],
|
|
376
|
+
"evidence": core["evidence"],
|
|
377
|
+
"truncated": model_result["truncated"],
|
|
378
|
+
"model_digest": model_result["model_digest"],
|
|
379
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Shared constants and filesystem helpers for yotta-dev-mcp."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
IGNORE_DIRS = {
|
|
11
|
+
".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv",
|
|
12
|
+
"dist", "build", ".next", ".nuxt", ".cache", ".tmp", ".tmp2",
|
|
13
|
+
}
|
|
14
|
+
SOURCE_EXTS = {
|
|
15
|
+
".py", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".sh", ".ps1",
|
|
16
|
+
".go", ".rs", ".java", ".kt", ".kts", ".rb", ".php",
|
|
17
|
+
}
|
|
18
|
+
TEXT_EXTS = SOURCE_EXTS | {".json", ".md", ".txt", ".yml", ".yaml", ".toml", ".ini", ".cfg"}
|
|
19
|
+
MAX_FILE_BYTES = 2 * 1024 * 1024
|
|
20
|
+
|
|
21
|
+
JS_EXTS = (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs")
|
|
22
|
+
CONFIG_NAMES = {
|
|
23
|
+
"package.json", "package-lock.json", "tsconfig.json", "pyproject.toml",
|
|
24
|
+
"setup.cfg", "requirements.txt", "requirements-dev.txt", "Cargo.toml",
|
|
25
|
+
"go.mod", "Makefile", "Dockerfile", "docker-compose.yml", "docker-compose.yaml",
|
|
26
|
+
".eslintrc.json", ".eslintrc.js", ".prettierrc", ".prettierrc.json",
|
|
27
|
+
}
|
|
28
|
+
CONFIG_SUFFIXES = (".toml", ".ini", ".cfg", ".yml", ".yaml")
|
|
29
|
+
STORAGE_SUFFIXES = {
|
|
30
|
+
".sqlite": "sqlite-file", ".sqlite3": "sqlite-file",
|
|
31
|
+
".db": "db-file", ".mdb": "db-file", ".dbf": "db-file",
|
|
32
|
+
}
|
|
33
|
+
TEST_NAME_RE = re.compile(
|
|
34
|
+
r"(?i)^test_.*\.(py|js|ts|jsx|tsx)$|_test\.py$|\.(test|spec)\.[jt]sx?$"
|
|
35
|
+
)
|
|
36
|
+
UNKNOWN_LIMIT = 50
|
|
37
|
+
EVIDENCE_LIMIT = 100
|
|
38
|
+
RISK_ENUM_WEIGHTS = {"critical": 5, "high": 4, "medium": 2, "low": 1}
|
|
39
|
+
CONE_LIMIT = 500
|
|
40
|
+
SYMBOL_MATCH_LIMIT = 20
|
|
41
|
+
IMPORT_KINDS_DECISIVE = ("internal", "internal-file")
|
|
42
|
+
BLAST_LEVELS = ((9, "critical"), (6, "high"), (3, "medium"))
|
|
43
|
+
VERIFY_LEVELS = ("L0", "L1", "L2", "L3", "L4", "L5")
|
|
44
|
+
VERIFY_EXEC_LEVELS = ("L2", "L3", "L4")
|
|
45
|
+
VERIFY_DEFAULT_LEVELS = ("L0", "L1")
|
|
46
|
+
VERIFY_REQUIRED_SOURCE_FILES = (
|
|
47
|
+
"SKILL.md", "package.json", "README.md", "README.zh-CN.md",
|
|
48
|
+
"CHANGELOG.md", "LICENSE", "NOTICE", "server.json", "install.sh",
|
|
49
|
+
"bin/yotta-dev-mcp.js", "bin/install.js",
|
|
50
|
+
"scripts/dev_engine.py", "scripts/yotta_dev_mcp.py",
|
|
51
|
+
"scripts/dev_adapters.py", "scripts/dev_common.py", "scripts/dev_model.py",
|
|
52
|
+
"scripts/dev_architecture.py", "scripts/dev_impact.py", "scripts/dev_verify.py",
|
|
53
|
+
"scripts/dev_selftest.py",
|
|
54
|
+
"references/tools.md", "references/adapters.md",
|
|
55
|
+
"references/architecture-contract.md", "assets/banner.png",
|
|
56
|
+
)
|
|
57
|
+
VERIFY_REQUIRED_INSTALLED_FILES = ("SKILL.md", "assets/banner.png")
|
|
58
|
+
VERIFY_WRITE_GATES = (
|
|
59
|
+
("run_checks", "allow_execute"),
|
|
60
|
+
("scaffold_skill", "apply"),
|
|
61
|
+
("workflow_state", "apply"),
|
|
62
|
+
("verify_change", "allow_execute"),
|
|
63
|
+
("run_adapter", "allow_execute"),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
|
67
|
+
ERROR_RE = re.compile(
|
|
68
|
+
r"(?i)(traceback|exception|\berror\b|\bfailed\b|\bfail\b|fatal|panic|"
|
|
69
|
+
r"assertion|npm err!|\berr\b|\bwarn(?:ing)?\b)"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def source_exts():
|
|
74
|
+
return set(SOURCE_EXTS)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _json_safe(value):
|
|
78
|
+
if isinstance(value, Path):
|
|
79
|
+
return str(value)
|
|
80
|
+
if isinstance(value, dict):
|
|
81
|
+
return {str(k): _json_safe(v) for k, v in value.items()}
|
|
82
|
+
if isinstance(value, list):
|
|
83
|
+
return [_json_safe(v) for v in value]
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _read_text(path):
|
|
88
|
+
path = Path(path)
|
|
89
|
+
if path.stat().st_size > MAX_FILE_BYTES:
|
|
90
|
+
raise ValueError("文件超过大小上限: %s" % path)
|
|
91
|
+
data = path.read_bytes()
|
|
92
|
+
if b"\x00" in data[:4096]:
|
|
93
|
+
raise ValueError("二进制文件不参与文本扫描: %s" % path)
|
|
94
|
+
return data.decode("utf-8", errors="replace")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _iter_files(root, extensions=None, max_files=5000, all_files=False):
|
|
98
|
+
root = Path(root)
|
|
99
|
+
extensions = set(extensions or TEXT_EXTS)
|
|
100
|
+
count = 0
|
|
101
|
+
for current, dirs, files in os.walk(str(root)):
|
|
102
|
+
dirs[:] = sorted(d for d in dirs if d not in IGNORE_DIRS)
|
|
103
|
+
for name in sorted(files):
|
|
104
|
+
path = Path(current) / name
|
|
105
|
+
if path.is_symlink():
|
|
106
|
+
continue
|
|
107
|
+
if not all_files and extensions and path.suffix.lower() not in extensions:
|
|
108
|
+
continue
|
|
109
|
+
if path.stat().st_size > MAX_FILE_BYTES:
|
|
110
|
+
continue
|
|
111
|
+
yield path
|
|
112
|
+
count += 1
|
|
113
|
+
if count >= max_files:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _rel(root, path):
|
|
118
|
+
try:
|
|
119
|
+
return str(Path(path).resolve().relative_to(Path(root).resolve())).replace("\\", "/")
|
|
120
|
+
except ValueError:
|
|
121
|
+
return str(Path(path)).replace("\\", "/")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _language(path):
|
|
125
|
+
ext = Path(path).suffix.lower()
|
|
126
|
+
if ext == ".py":
|
|
127
|
+
return "python"
|
|
128
|
+
if ext in (".js", ".jsx", ".mjs", ".cjs"):
|
|
129
|
+
return "javascript"
|
|
130
|
+
if ext in (".ts", ".tsx"):
|
|
131
|
+
return "typescript"
|
|
132
|
+
if ext in (".sh", ".ps1"):
|
|
133
|
+
return "shell"
|
|
134
|
+
return ext.lstrip(".") or "text"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _frontmatter_version(text):
|
|
138
|
+
match = re.search(r"(?m)^version:\s*[\"']?([^\"'\r\n]+)", text)
|
|
139
|
+
return match.group(1).strip() if match else None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _frontmatter_name(text):
|
|
143
|
+
match = re.search(r"(?m)^name:\s*[\"']?([^\"'\r\n]+)", text)
|
|
144
|
+
return match.group(1).strip() if match else None
|