tzero-mcp 10.7.0 → 10.7.2
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/CHANGELOG.md +589 -0
- package/CONTRIBUTING.md +71 -0
- package/README.md +25 -17
- package/bin/postinstall.js +34 -0
- package/bin/tzero-mcp.js +16 -19
- package/bin/tzero.js +14 -19
- package/index.d.ts +24 -0
- package/index.js +7 -3
- package/package.json +24 -2
- package/src/addmcp.py +1194 -0
- package/src/main.py +60 -0
- package/src/siber_akademi_icon.py +77 -0
- package/src/tzero/jarvis/semantic_compressor.py +373 -0
- package/src/tzero.py +58 -0
- package/src/tzero_benchmark.py +475 -0
- package/src/tzero_bridge.py +362 -0
- package/src/tzero_ci.py +153 -0
- package/src/tzero_commit.py +192 -0
- package/src/tzero_cost.py +163 -0
- package/src/tzero_dashboard.py +1448 -0
- package/src/tzero_deps.py +287 -0
- package/src/tzero_drift.py +208 -0
- package/src/tzero_explain.py +240 -0
- package/src/tzero_features.py +3022 -0
- package/src/tzero_fix.py +228 -0
- package/src/tzero_fixer/__init__.py +63 -0
- package/src/tzero_fixer/amalgamation.py +334 -0
- package/src/tzero_fixer/ast_syntax_healer.py +464 -0
- package/src/tzero_fixer/ast_type_modernizer.py +288 -0
- package/src/tzero_fixer/async_io_modernizer.py +127 -0
- package/src/tzero_fixer/automated_regression_suite.py +600 -0
- package/src/tzero_fixer/benchmarks_and_diagnostics.py +174 -0
- package/src/tzero_fixer/ci_cd_self_healing_gate.py +246 -0
- package/src/tzero_fixer/code_duplicity_and_refactorer.py +151 -0
- package/src/tzero_fixer/code_smells_healer.py +260 -0
- package/src/tzero_fixer/cwe_remediator.py +171 -0
- package/src/tzero_fixer/dead_code_eliminator.py +150 -0
- package/src/tzero_fixer/dependency_vulnerability_auditor.py +309 -0
- package/src/tzero_fixer/git_advanced_surgeon.py +280 -0
- package/src/tzero_fixer/hardware_and_disk_doctor.py +325 -0
- package/src/tzero_fixer/import_graph_healer.py +412 -0
- package/src/tzero_fixer/interactive_tui.py +91 -0
- package/src/tzero_fixer/knowledge_base.py +295 -0
- package/src/tzero_fixer/knowledge_base_extended.py +940 -0
- package/src/tzero_fixer/linters_and_static_analyzer.py +307 -0
- package/src/tzero_fixer/mcp_server.py +447 -0
- package/src/tzero_fixer/models.py +385 -0
- package/src/tzero_fixer/orchestrator.py +375 -0
- package/src/tzero_fixer/os_doctor.py +502 -0
- package/src/tzero_fixer/packaging_abi_surgeon.py +323 -0
- package/src/tzero_fixer/pc_fixer_doctor.py +599 -0
- package/src/tzero_fixer/performance_and_memory_profiler.py +274 -0
- package/src/tzero_fixer/polyglot_ast_parsers.py +313 -0
- package/src/tzero_fixer/polyglot_build_healer.py +364 -0
- package/src/tzero_fixer/polyglot_config_healer.py +318 -0
- package/src/tzero_fixer/polyglot_doctor.py +372 -0
- package/src/tzero_fixer/polyglot_linters.py +619 -0
- package/src/tzero_fixer/polyglot_schema_validator.py +305 -0
- package/src/tzero_fixer/security_cwe_database.py +444 -0
- package/src/tzero_fixer/security_deep_rules.py +114 -0
- package/src/tzero_fixer/security_rules_catalog.py +110 -0
- package/src/tzero_fixer/security_zero_leak.py +326 -0
- package/src/tzero_fixer/smart_patch_recommender.py +223 -0
- package/src/tzero_fixer/synthetic_workspace_generator.py +220 -0
- package/src/tzero_fixer/system_diagnostics_extended.py +493 -0
- package/src/tzero_fixer/system_repair_recipes.py +447 -0
- package/src/tzero_fixer/venv_doctor.py +309 -0
- package/src/tzero_gateway.py +885 -0
- package/src/tzero_guard.py +243 -0
- package/src/tzero_infra.py +274 -0
- package/src/tzero_jarvis/__init__.py +124 -0
- package/src/tzero_jarvis/causal_graph.py +310 -0
- package/src/tzero_jarvis/graph_visualizer.py +395 -0
- package/src/tzero_jarvis/hybrid_search.py +214 -0
- package/src/tzero_jarvis/latent_press.py +137 -0
- package/src/tzero_jarvis/memory_consolidation.py +212 -0
- package/src/tzero_jarvis/minhash_lsh.py +184 -0
- package/src/tzero_jarvis/orchestrator.py +335 -0
- package/src/tzero_jarvis/self_healing.py +308 -0
- package/src/tzero_jarvis/semantic_compressor.py +887 -0
- package/src/tzero_jarvis/swarm.py +261 -0
- package/src/tzero_jarvis/swarm_bft.py +144 -0
- package/src/tzero_jarvis/workspace_orchestrator.py +1167 -0
- package/src/tzero_jarvis/zero_key_router.py +173 -0
- package/src/tzero_mcp.py +4858 -0
- package/src/tzero_neural_cortex.py +1425 -0
- package/src/tzero_packer.py +370 -0
- package/src/tzero_patch.py +624 -0
- package/src/tzero_pc_fixer.py +12657 -0
- package/src/tzero_pipe.py +95 -0
- package/src/tzero_polyglot.py +422 -0
- package/src/tzero_reachability.py +272 -0
- package/src/tzero_replay.py +141 -0
- package/src/tzero_security.py +570 -0
- package/src/tzero_snapshot.py +258 -0
- package/src/tzero_stubs.py +313 -0
- package/src/tzero_superdesign.py +774 -0
- package/src/tzero_sync.py +241 -0
- package/src/tzero_task_benchmark.py +363 -0
- package/src/tzero_tui.py +244 -0
- package/src/tzero_v3.py +9435 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
T-Zero Polyglot Build, Container, & Infrastructure Healer
|
|
4
|
+
========================================================
|
|
5
|
+
Specialized AST & syntax healer for cross-language build tools, Dockerfiles,
|
|
6
|
+
Makefiles, container configurations, web server rules, and multi-ecosystem manifests.
|
|
7
|
+
|
|
8
|
+
Key Capabilities:
|
|
9
|
+
- Dockerfile best-practice auditor & .dockerignore generator
|
|
10
|
+
- Makefile tab-vs-space indentation healer (eliminates 'missing separator' errors)
|
|
11
|
+
- Makefile .PHONY target generator
|
|
12
|
+
- Nginx / Caddy reverse proxy syntax and semicolon validator
|
|
13
|
+
- Rust Cargo.toml & Go go.mod basic syntax integrity checkers
|
|
14
|
+
|
|
15
|
+
Author: Siber Akademi T-Zero R&D Team
|
|
16
|
+
Version: 10.5.0 | 100% Free & Local Offline | Zero-Leak Certified
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
26
|
+
|
|
27
|
+
from tzero_fixer.models import (
|
|
28
|
+
DiagnosticCategory,
|
|
29
|
+
DiagnosticIssue,
|
|
30
|
+
HealAction,
|
|
31
|
+
SeverityLevel,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PolyglotBuildHealer:
|
|
36
|
+
"""Surgeon for polyglot build scripts, container files, and infrastructure recipes."""
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def diagnose_workspace(cls, workspace_path: str) -> List[DiagnosticIssue]:
|
|
40
|
+
"""Audit build and container files across the workspace."""
|
|
41
|
+
issues: List[DiagnosticIssue] = []
|
|
42
|
+
root = Path(workspace_path).resolve()
|
|
43
|
+
|
|
44
|
+
# 1. Dockerfile check
|
|
45
|
+
for dockerfile in root.glob("**/Dockerfile*"):
|
|
46
|
+
if dockerfile.is_file() and not cls._is_ignored(dockerfile):
|
|
47
|
+
issues.extend(cls._audit_dockerfile(dockerfile, root))
|
|
48
|
+
|
|
49
|
+
# 2. Makefile check
|
|
50
|
+
for makefile in root.glob("**/Makefile*"):
|
|
51
|
+
if makefile.is_file() and not cls._is_ignored(makefile):
|
|
52
|
+
issues.extend(cls._audit_makefile(makefile))
|
|
53
|
+
|
|
54
|
+
# 3. Nginx / Caddy conf check
|
|
55
|
+
for conf in root.glob("**/*.conf"):
|
|
56
|
+
if conf.is_file() and not cls._is_ignored(conf):
|
|
57
|
+
issues.extend(cls._audit_webserver_conf(conf))
|
|
58
|
+
|
|
59
|
+
# 4. Cargo.toml check
|
|
60
|
+
for cargo in root.glob("**/Cargo.toml"):
|
|
61
|
+
if cargo.is_file() and not cls._is_ignored(cargo):
|
|
62
|
+
issues.extend(cls._audit_cargo_toml(cargo))
|
|
63
|
+
|
|
64
|
+
# 5. go.mod check
|
|
65
|
+
for gopkg in root.glob("**/go.mod"):
|
|
66
|
+
if gopkg.is_file() and not cls._is_ignored(gopkg):
|
|
67
|
+
issues.extend(cls._audit_go_mod(gopkg))
|
|
68
|
+
|
|
69
|
+
return issues
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def _is_ignored(cls, p: Path) -> bool:
|
|
73
|
+
"""Check if path is inside ignored VCS, venv, or build folders."""
|
|
74
|
+
parts = p.parts
|
|
75
|
+
ignored_names = {".git", ".venv", "venv", "node_modules", "dist", "build", ".tox", "__pycache__"}
|
|
76
|
+
return any(part in ignored_names for part in parts)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def _audit_dockerfile(cls, dockerfile: Path, root: Path) -> List[DiagnosticIssue]:
|
|
80
|
+
"""Audit Dockerfile for best practices and check for .dockerignore."""
|
|
81
|
+
issues: List[DiagnosticIssue] = []
|
|
82
|
+
try:
|
|
83
|
+
content = dockerfile.read_text(encoding="utf-8", errors="replace")
|
|
84
|
+
lines = content.splitlines()
|
|
85
|
+
except Exception:
|
|
86
|
+
return issues
|
|
87
|
+
|
|
88
|
+
# Check for missing .dockerignore in directory
|
|
89
|
+
dockerignore = dockerfile.parent / ".dockerignore"
|
|
90
|
+
if not dockerignore.is_file():
|
|
91
|
+
issues.append(DiagnosticIssue(
|
|
92
|
+
category=DiagnosticCategory.WORKSPACE,
|
|
93
|
+
severity=SeverityLevel.WARNING,
|
|
94
|
+
description=f"Dockerfile in '{dockerfile.parent.name}' lacks a '.dockerignore' file (unnecessary files like .git or .venv will bloat context)",
|
|
95
|
+
file_path=str(dockerfile),
|
|
96
|
+
suggested_fix="Generate optimized .dockerignore to exclude git, venv, cache, and build artifacts",
|
|
97
|
+
metadata={"issue_type": "missing_dockerignore", "target_dir": str(dockerfile.parent)},
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
for idx, line in enumerate(lines, start=1):
|
|
101
|
+
stripped = line.strip()
|
|
102
|
+
|
|
103
|
+
# FROM :latest warning
|
|
104
|
+
if stripped.startswith("FROM ") and ":latest" in stripped:
|
|
105
|
+
issues.append(DiagnosticIssue(
|
|
106
|
+
category=DiagnosticCategory.WORKSPACE,
|
|
107
|
+
severity=SeverityLevel.INFO,
|
|
108
|
+
description=f"Dockerfile uses unpinned ':latest' base image: '{stripped}'",
|
|
109
|
+
file_path=str(dockerfile),
|
|
110
|
+
line_number=idx,
|
|
111
|
+
suggested_fix="Pin base image to specific digest or version tag for deterministic builds",
|
|
112
|
+
metadata={"issue_type": "unpinned_docker_image"},
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
# ADD instead of COPY
|
|
116
|
+
if stripped.startswith("ADD ") and not ("://" in stripped or stripped.endswith(".tar.gz") or stripped.endswith(".tgz")):
|
|
117
|
+
issues.append(DiagnosticIssue(
|
|
118
|
+
category=DiagnosticCategory.WORKSPACE,
|
|
119
|
+
severity=SeverityLevel.INFO,
|
|
120
|
+
description=f"Using 'ADD' instead of 'COPY' for simple local file copy: '{stripped}'",
|
|
121
|
+
file_path=str(dockerfile),
|
|
122
|
+
line_number=idx,
|
|
123
|
+
suggested_fix="Replace 'ADD' with 'COPY' for local files as per Dockerfile best practices",
|
|
124
|
+
metadata={"issue_type": "docker_add_to_copy", "line_num": idx},
|
|
125
|
+
))
|
|
126
|
+
|
|
127
|
+
# apt-get install without cleanup
|
|
128
|
+
if "apt-get install" in stripped and "rm -rf /var/lib/apt/lists" not in stripped and not stripped.endswith("\\"):
|
|
129
|
+
issues.append(DiagnosticIssue(
|
|
130
|
+
category=DiagnosticCategory.WORKSPACE,
|
|
131
|
+
severity=SeverityLevel.WARNING,
|
|
132
|
+
description=f"apt-get install without cache cleanup in same RUN layer: '{stripped}'",
|
|
133
|
+
file_path=str(dockerfile),
|
|
134
|
+
line_number=idx,
|
|
135
|
+
suggested_fix="Append '&& rm -rf /var/lib/apt/lists/*' to reduce container image footprint",
|
|
136
|
+
metadata={"issue_type": "docker_apt_cleanup"},
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
return issues
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def _audit_makefile(cls, makefile: Path) -> List[DiagnosticIssue]:
|
|
143
|
+
"""Audit Makefile for space vs tab indentation and missing .PHONY declarations."""
|
|
144
|
+
issues: List[DiagnosticIssue] = []
|
|
145
|
+
try:
|
|
146
|
+
content = makefile.read_text(encoding="utf-8", errors="replace")
|
|
147
|
+
lines = content.splitlines()
|
|
148
|
+
except Exception:
|
|
149
|
+
return issues
|
|
150
|
+
|
|
151
|
+
in_rule = False
|
|
152
|
+
space_indented_lines: List[int] = []
|
|
153
|
+
targets: List[str] = []
|
|
154
|
+
|
|
155
|
+
for idx, line in enumerate(lines, start=1):
|
|
156
|
+
if not line.strip() or line.strip().startswith("#"):
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
# Detect target rule
|
|
160
|
+
target_match = re.match(r"^([a-zA-Z0-9_\-\.]+)\s*:\s*(.*)$", line)
|
|
161
|
+
if target_match:
|
|
162
|
+
in_rule = True
|
|
163
|
+
tgt = target_match.group(1)
|
|
164
|
+
if not tgt.startswith("."):
|
|
165
|
+
targets.append(tgt)
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# Recipe lines must start with TAB, not spaces
|
|
169
|
+
if in_rule:
|
|
170
|
+
if line.startswith(" ") or (line.startswith(" ") and not line.startswith("\t")):
|
|
171
|
+
space_indented_lines.append(idx)
|
|
172
|
+
|
|
173
|
+
if space_indented_lines:
|
|
174
|
+
issues.append(DiagnosticIssue(
|
|
175
|
+
category=DiagnosticCategory.SYNTAX,
|
|
176
|
+
severity=SeverityLevel.ERROR,
|
|
177
|
+
description=f"Makefile contains {len(space_indented_lines)} recipe line(s) indented with spaces instead of tabs (causes '*** missing separator' error)",
|
|
178
|
+
file_path=str(makefile),
|
|
179
|
+
line_number=space_indented_lines[0],
|
|
180
|
+
suggested_fix="Convert leading spaces on recipe lines to real tab (\\t) characters",
|
|
181
|
+
metadata={"issue_type": "makefile_spaces_to_tabs", "lines": space_indented_lines},
|
|
182
|
+
))
|
|
183
|
+
|
|
184
|
+
if targets and ".PHONY:" not in content:
|
|
185
|
+
issues.append(DiagnosticIssue(
|
|
186
|
+
category=DiagnosticCategory.WORKSPACE,
|
|
187
|
+
severity=SeverityLevel.INFO,
|
|
188
|
+
description="Makefile declares targets without '.PHONY' directive (can conflict with files named after targets)",
|
|
189
|
+
file_path=str(makefile),
|
|
190
|
+
suggested_fix=f"Declare targets as .PHONY: {', '.join(targets[:5])}",
|
|
191
|
+
metadata={"issue_type": "makefile_missing_phony", "targets": targets},
|
|
192
|
+
))
|
|
193
|
+
|
|
194
|
+
return issues
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def _audit_webserver_conf(cls, conf_file: Path) -> List[DiagnosticIssue]:
|
|
198
|
+
"""Audit Nginx / reverse proxy config files for missing semicolons."""
|
|
199
|
+
issues: List[DiagnosticIssue] = []
|
|
200
|
+
try:
|
|
201
|
+
content = conf_file.read_text(encoding="utf-8", errors="replace")
|
|
202
|
+
lines = content.splitlines()
|
|
203
|
+
except Exception:
|
|
204
|
+
return issues
|
|
205
|
+
|
|
206
|
+
# Only audit if it looks like an nginx/caddy configuration
|
|
207
|
+
if "server {" not in content and "http {" not in content and "location " not in content:
|
|
208
|
+
return issues
|
|
209
|
+
|
|
210
|
+
for idx, line in enumerate(lines, start=1):
|
|
211
|
+
stripped = line.strip()
|
|
212
|
+
if not stripped or stripped.startswith("#") or stripped.endswith("{") or stripped.endswith("}"):
|
|
213
|
+
continue
|
|
214
|
+
if not stripped.endswith(";"):
|
|
215
|
+
issues.append(DiagnosticIssue(
|
|
216
|
+
category=DiagnosticCategory.SYNTAX,
|
|
217
|
+
severity=SeverityLevel.ERROR,
|
|
218
|
+
description=f"Missing terminating semicolon in Nginx directive: '{stripped}'",
|
|
219
|
+
file_path=str(conf_file),
|
|
220
|
+
line_number=idx,
|
|
221
|
+
suggested_fix="Append ';' to directive line",
|
|
222
|
+
metadata={"issue_type": "nginx_missing_semicolon", "line_num": idx},
|
|
223
|
+
))
|
|
224
|
+
|
|
225
|
+
return issues
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def _audit_cargo_toml(cls, cargo_file: Path) -> List[DiagnosticIssue]:
|
|
229
|
+
"""Audit Rust Cargo.toml for basic sections."""
|
|
230
|
+
issues: List[DiagnosticIssue] = []
|
|
231
|
+
try:
|
|
232
|
+
content = cargo_file.read_text(encoding="utf-8", errors="replace")
|
|
233
|
+
except Exception:
|
|
234
|
+
return issues
|
|
235
|
+
|
|
236
|
+
if "[package]" not in content and "[workspace]" not in content:
|
|
237
|
+
issues.append(DiagnosticIssue(
|
|
238
|
+
category=DiagnosticCategory.DEPENDENCY,
|
|
239
|
+
severity=SeverityLevel.ERROR,
|
|
240
|
+
description="Cargo.toml is missing mandatory '[package]' or '[workspace]' header",
|
|
241
|
+
file_path=str(cargo_file),
|
|
242
|
+
suggested_fix="Add '[package]' table with name, version, and edition = '2021'",
|
|
243
|
+
metadata={"issue_type": "cargo_missing_package"},
|
|
244
|
+
))
|
|
245
|
+
|
|
246
|
+
return issues
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def _audit_go_mod(cls, go_mod_file: Path) -> List[DiagnosticIssue]:
|
|
250
|
+
"""Audit Go go.mod for module declaration."""
|
|
251
|
+
issues: List[DiagnosticIssue] = []
|
|
252
|
+
try:
|
|
253
|
+
content = go_mod_file.read_text(encoding="utf-8", errors="replace")
|
|
254
|
+
except Exception:
|
|
255
|
+
return issues
|
|
256
|
+
|
|
257
|
+
if not re.search(r"^module\s+\S+", content, re.MULTILINE):
|
|
258
|
+
issues.append(DiagnosticIssue(
|
|
259
|
+
category=DiagnosticCategory.DEPENDENCY,
|
|
260
|
+
severity=SeverityLevel.ERROR,
|
|
261
|
+
description="go.mod is missing mandatory 'module <path>' declaration",
|
|
262
|
+
file_path=str(go_mod_file),
|
|
263
|
+
suggested_fix="Declare module path on first line of go.mod",
|
|
264
|
+
metadata={"issue_type": "go_missing_module"},
|
|
265
|
+
))
|
|
266
|
+
|
|
267
|
+
return issues
|
|
268
|
+
|
|
269
|
+
@classmethod
|
|
270
|
+
def heal_issue(cls, issue: DiagnosticIssue, dry_run: bool = False) -> HealAction:
|
|
271
|
+
"""Autonomously heal polyglot build and container issues."""
|
|
272
|
+
itype = issue.metadata.get("issue_type")
|
|
273
|
+
file_path = issue.file_path
|
|
274
|
+
|
|
275
|
+
if itype == "missing_dockerignore":
|
|
276
|
+
target_dir = Path(issue.metadata.get("target_dir", "."))
|
|
277
|
+
dignore = target_dir / ".dockerignore"
|
|
278
|
+
if dry_run:
|
|
279
|
+
return HealAction(issue=issue, success=True, details=f"[Dry-Run] Would create .dockerignore in {target_dir}")
|
|
280
|
+
|
|
281
|
+
ignore_content = (
|
|
282
|
+
"# T-Zero Autonomous Dockerignore\n"
|
|
283
|
+
".git\n"
|
|
284
|
+
".gitignore\n"
|
|
285
|
+
".venv\n"
|
|
286
|
+
"venv/\n"
|
|
287
|
+
"env/\n"
|
|
288
|
+
"node_modules/\n"
|
|
289
|
+
"__pycache__/\n"
|
|
290
|
+
"*.pyc\n"
|
|
291
|
+
"*.pyo\n"
|
|
292
|
+
"dist/\n"
|
|
293
|
+
"build/\n"
|
|
294
|
+
"*.egg-info/\n"
|
|
295
|
+
".DS_Store\n"
|
|
296
|
+
"Thumbs.db\n"
|
|
297
|
+
".env\n"
|
|
298
|
+
".env.local\n"
|
|
299
|
+
"coverage/\n"
|
|
300
|
+
".pytest_cache/\n"
|
|
301
|
+
)
|
|
302
|
+
dignore.write_text(ignore_content, encoding="utf-8")
|
|
303
|
+
return HealAction(issue=issue, success=True, details=f"Generated high-performance .dockerignore in '{target_dir.name}'")
|
|
304
|
+
|
|
305
|
+
if itype == "makefile_spaces_to_tabs" and file_path:
|
|
306
|
+
p = Path(file_path)
|
|
307
|
+
if dry_run:
|
|
308
|
+
return HealAction(issue=issue, success=True, details="[Dry-Run] Would convert recipe spaces to tabs in Makefile")
|
|
309
|
+
|
|
310
|
+
lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
311
|
+
new_lines = []
|
|
312
|
+
converted = 0
|
|
313
|
+
in_rule = False
|
|
314
|
+
|
|
315
|
+
for line in lines:
|
|
316
|
+
if not line.strip() or line.strip().startswith("#"):
|
|
317
|
+
new_lines.append(line)
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
if re.match(r"^[a-zA-Z0-9_\-\.]+\s*:\s*.*$", line):
|
|
321
|
+
in_rule = True
|
|
322
|
+
new_lines.append(line)
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
if in_rule:
|
|
326
|
+
if line.startswith(" "):
|
|
327
|
+
new_lines.append("\t" + line[4:])
|
|
328
|
+
converted += 1
|
|
329
|
+
continue
|
|
330
|
+
elif line.startswith(" "):
|
|
331
|
+
new_lines.append("\t" + line.lstrip(" "))
|
|
332
|
+
converted += 1
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
new_lines.append(line)
|
|
336
|
+
|
|
337
|
+
p.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
|
|
338
|
+
return HealAction(issue=issue, success=True, details=f"Converted {converted} recipe lines from spaces to real tabs in Makefile")
|
|
339
|
+
|
|
340
|
+
if itype == "makefile_missing_phony" and file_path:
|
|
341
|
+
p = Path(file_path)
|
|
342
|
+
targets = issue.metadata.get("targets", [])
|
|
343
|
+
if dry_run:
|
|
344
|
+
return HealAction(issue=issue, success=True, details=f"[Dry-Run] Would prepend .PHONY: {' '.join(targets)} to Makefile")
|
|
345
|
+
|
|
346
|
+
content = p.read_text(encoding="utf-8", errors="replace")
|
|
347
|
+
phony_line = f".PHONY: {' '.join(targets)}\n\n"
|
|
348
|
+
p.write_text(phony_line + content, encoding="utf-8")
|
|
349
|
+
return HealAction(issue=issue, success=True, details=f"Injected .PHONY target declarations into Makefile")
|
|
350
|
+
|
|
351
|
+
if itype == "docker_add_to_copy" and file_path:
|
|
352
|
+
p = Path(file_path)
|
|
353
|
+
line_num = issue.metadata.get("line_num")
|
|
354
|
+
if dry_run:
|
|
355
|
+
return HealAction(issue=issue, success=True, details=f"[Dry-Run] Would replace ADD with COPY on line {line_num}")
|
|
356
|
+
|
|
357
|
+
lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
358
|
+
if line_num and 1 <= line_num <= len(lines):
|
|
359
|
+
lines[line_num - 1] = re.sub(r"^ADD\s+", "COPY ", lines[line_num - 1])
|
|
360
|
+
p.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
361
|
+
return HealAction(issue=issue, success=True, details=f"Replaced 'ADD' with 'COPY' on line {line_num} of Dockerfile")
|
|
362
|
+
|
|
363
|
+
return HealAction(issue=issue, success=False, details=f"No automated repair for issue type '{itype}'")
|
|
364
|
+
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""T-Zero Polyglot Configuration & Environment Files Healer.
|
|
3
|
+
|
|
4
|
+
Performs forensic repair and hygiene checks on configuration files:
|
|
5
|
+
- JSON repair engine (strips trailing commas, single quotes, unquoted keys, and JS comments).
|
|
6
|
+
- Gitignore & Security hygiene (verifies .env, *.pem, *.key are in .gitignore).
|
|
7
|
+
- Gitattributes LF/CRLF normalizer (* text=auto eol=lf).
|
|
8
|
+
- EditorConfig generator and validator.
|
|
9
|
+
|
|
10
|
+
Author: Toprak Ahmet Aydogmus (Siber Akademi)
|
|
11
|
+
Version: 10.4.0
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import difflib
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
22
|
+
|
|
23
|
+
from .models import (
|
|
24
|
+
DiagnosticCategory,
|
|
25
|
+
DiagnosticIssue,
|
|
26
|
+
HealAction,
|
|
27
|
+
SeverityLevel,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PolyglotConfigHealer:
|
|
32
|
+
"""Specialized repair engine for JSON, YAML, Gitignore, and Gitattributes files."""
|
|
33
|
+
|
|
34
|
+
SENSITIVE_GITIGNORE_ENTRIES = [
|
|
35
|
+
".env",
|
|
36
|
+
".env.local",
|
|
37
|
+
"*.pem",
|
|
38
|
+
"*.key",
|
|
39
|
+
"*.pfx",
|
|
40
|
+
"*.p12",
|
|
41
|
+
"id_rsa",
|
|
42
|
+
"id_ed25519",
|
|
43
|
+
".tzero/fixer_backups/",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# -------------------------------------------------------------------------
|
|
47
|
+
# 1. JSON SURGICAL REPAIR ENGINE
|
|
48
|
+
# -------------------------------------------------------------------------
|
|
49
|
+
@classmethod
|
|
50
|
+
def heal_json_string(cls, content: str) -> Any:
|
|
51
|
+
"""Helper to repair trailing commas and single quotes in JSON strings."""
|
|
52
|
+
@dataclass
|
|
53
|
+
class JsonHealResult:
|
|
54
|
+
original_content: str
|
|
55
|
+
repaired_content: str
|
|
56
|
+
changed: bool
|
|
57
|
+
|
|
58
|
+
changed, repaired = cls.repair_json_text(content)
|
|
59
|
+
return JsonHealResult(
|
|
60
|
+
original_content=content,
|
|
61
|
+
repaired_content=repaired,
|
|
62
|
+
changed=changed,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def repair_json_text(cls, text: str) -> Tuple[bool, str]:
|
|
67
|
+
"""Surgically repair trailing commas, single quotes, and comments in JSON."""
|
|
68
|
+
repaired = text
|
|
69
|
+
|
|
70
|
+
# Strip line comments // ...
|
|
71
|
+
repaired = re.sub(r"^\s*//.*$", "", repaired, flags=re.MULTILINE)
|
|
72
|
+
# Strip block comments /* ... */
|
|
73
|
+
repaired = re.sub(r"/\*.*?\*/", "", repaired, flags=re.DOTALL)
|
|
74
|
+
|
|
75
|
+
# Convert single quotes to double quotes (around keys/values)
|
|
76
|
+
repaired = re.sub(r"\'([^\'\\]*)\'", r'"\1"', repaired)
|
|
77
|
+
|
|
78
|
+
# Strip trailing commas in objects: , } -> }
|
|
79
|
+
repaired = re.sub(r",\s*(\})", r"\1", repaired)
|
|
80
|
+
# Strip trailing commas in arrays: , ] -> ]
|
|
81
|
+
repaired = re.sub(r",\s*(\])", r"\1", repaired)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
json.loads(repaired)
|
|
85
|
+
return True, repaired
|
|
86
|
+
except Exception:
|
|
87
|
+
return False, text
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def check_json_files(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
91
|
+
"""Inspect all JSON files in workspace for syntax errors and trailing commas."""
|
|
92
|
+
issues: List[DiagnosticIssue] = []
|
|
93
|
+
root = Path(workspace_dir).resolve()
|
|
94
|
+
|
|
95
|
+
for jf in root.glob("**/*.json"):
|
|
96
|
+
if not jf.is_file() or any(p in jf.parts for p in (".git", "node_modules", ".venv", "venv", "dist", "build")):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
with open(jf, "r", encoding="utf-8") as f:
|
|
101
|
+
content = f.read()
|
|
102
|
+
json.loads(content)
|
|
103
|
+
except json.JSONDecodeError as jde:
|
|
104
|
+
can_heal, _ = cls.repair_json_text(content)
|
|
105
|
+
issues.append(DiagnosticIssue(
|
|
106
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
107
|
+
severity=SeverityLevel.ERROR.value,
|
|
108
|
+
file_path=str(jf),
|
|
109
|
+
line=jde.lineno,
|
|
110
|
+
column=jde.colno,
|
|
111
|
+
error_code="TZ-JSON-SYNTAX-ERROR",
|
|
112
|
+
message=f"JSON syntax error in {jf.name}: {jde.msg}",
|
|
113
|
+
suggested_fix="Surgically heal trailing commas, comments, and quotes" if can_heal else "Fix JSON structure",
|
|
114
|
+
auto_fixable=can_heal,
|
|
115
|
+
metadata={"action": "repair_json_file", "path": str(jf)}
|
|
116
|
+
))
|
|
117
|
+
except Exception:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
return issues
|
|
121
|
+
|
|
122
|
+
# -------------------------------------------------------------------------
|
|
123
|
+
# 2. GITIGNORE HYGIENE DOCTOR
|
|
124
|
+
# -------------------------------------------------------------------------
|
|
125
|
+
@classmethod
|
|
126
|
+
def check_gitignore_hygiene(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
127
|
+
"""Verify .gitignore exists and contains critical secret patterns (.env, *.key)."""
|
|
128
|
+
issues: List[DiagnosticIssue] = []
|
|
129
|
+
root = Path(workspace_dir).resolve()
|
|
130
|
+
gi_file = root / ".gitignore"
|
|
131
|
+
|
|
132
|
+
if not gi_file.is_file():
|
|
133
|
+
issues.append(DiagnosticIssue(
|
|
134
|
+
category=DiagnosticCategory.SECURITY.value,
|
|
135
|
+
severity=SeverityLevel.ERROR.value,
|
|
136
|
+
file_path=str(root),
|
|
137
|
+
line=None,
|
|
138
|
+
column=None,
|
|
139
|
+
error_code="TZ-GIT-MISSING-GITIGNORE",
|
|
140
|
+
message="No '.gitignore' file found in workspace root (risk of committing secrets)",
|
|
141
|
+
suggested_fix="Generate comprehensive .gitignore with .env and secret patterns",
|
|
142
|
+
auto_fixable=True,
|
|
143
|
+
metadata={"action": "create_gitignore", "path": str(gi_file)}
|
|
144
|
+
))
|
|
145
|
+
return issues
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
with open(gi_file, "r", encoding="utf-8", errors="replace") as f:
|
|
149
|
+
content = f.read()
|
|
150
|
+
lines = set(line.strip() for line in content.splitlines() if line.strip())
|
|
151
|
+
|
|
152
|
+
missing_entries = []
|
|
153
|
+
for entry in cls.SENSITIVE_GITIGNORE_ENTRIES:
|
|
154
|
+
if entry not in lines and not any(l.startswith(entry) for l in lines):
|
|
155
|
+
missing_entries.append(entry)
|
|
156
|
+
|
|
157
|
+
if missing_entries:
|
|
158
|
+
issues.append(DiagnosticIssue(
|
|
159
|
+
category=DiagnosticCategory.SECURITY.value,
|
|
160
|
+
severity=SeverityLevel.WARNING.value,
|
|
161
|
+
file_path=str(gi_file),
|
|
162
|
+
line=1,
|
|
163
|
+
column=0,
|
|
164
|
+
error_code="TZ-GIT-UNPROTECTED-SECRETS",
|
|
165
|
+
message=f".gitignore is missing critical secret patterns: {', '.join(missing_entries[:4])}",
|
|
166
|
+
suggested_fix=f"Append {', '.join(missing_entries)} to .gitignore",
|
|
167
|
+
auto_fixable=True,
|
|
168
|
+
metadata={"action": "append_gitignore_entries", "entries": missing_entries, "path": str(gi_file)}
|
|
169
|
+
))
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
return issues
|
|
174
|
+
|
|
175
|
+
# -------------------------------------------------------------------------
|
|
176
|
+
# 3. GITATTRIBUTES LF/CRLF NORMALIZER
|
|
177
|
+
# -------------------------------------------------------------------------
|
|
178
|
+
@classmethod
|
|
179
|
+
def check_gitattributes(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
180
|
+
"""Verify .gitattributes exists to prevent cross-platform CRLF/LF chaos."""
|
|
181
|
+
issues: List[DiagnosticIssue] = []
|
|
182
|
+
root = Path(workspace_dir).resolve()
|
|
183
|
+
ga_file = root / ".gitattributes"
|
|
184
|
+
|
|
185
|
+
if not ga_file.is_file():
|
|
186
|
+
issues.append(DiagnosticIssue(
|
|
187
|
+
category=DiagnosticCategory.ENVIRONMENT.value,
|
|
188
|
+
severity=SeverityLevel.INFO.value,
|
|
189
|
+
file_path=str(ga_file),
|
|
190
|
+
line=None,
|
|
191
|
+
column=None,
|
|
192
|
+
error_code="TZ-GIT-MISSING-GITATTRIBUTES",
|
|
193
|
+
message="No '.gitattributes' found; cross-platform line endings (CRLF vs LF) may drift",
|
|
194
|
+
suggested_fix="Generate standard .gitattributes ('* text=auto eol=lf')",
|
|
195
|
+
auto_fixable=True,
|
|
196
|
+
metadata={"action": "create_gitattributes", "path": str(ga_file)}
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
return issues
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def diagnose_workspace(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
203
|
+
"""Run all polyglot configuration and git hygiene audits."""
|
|
204
|
+
issues: List[DiagnosticIssue] = []
|
|
205
|
+
issues.extend(cls.check_json_files(workspace_dir))
|
|
206
|
+
issues.extend(cls.check_gitignore_hygiene(workspace_dir))
|
|
207
|
+
issues.extend(cls.check_gitattributes(workspace_dir))
|
|
208
|
+
return issues
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def execute_config_heals(cls, issues: List[DiagnosticIssue], dry_run: bool = False) -> List[HealAction]:
|
|
212
|
+
"""Execute repairs for JSON files, .gitignore, and .gitattributes."""
|
|
213
|
+
actions: List[HealAction] = []
|
|
214
|
+
|
|
215
|
+
for issue in issues:
|
|
216
|
+
action_type = issue.metadata.get("action")
|
|
217
|
+
file_p = issue.metadata.get("path")
|
|
218
|
+
if not issue.auto_fixable or not action_type or not file_p:
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
t0 = time.time()
|
|
222
|
+
p = Path(file_p)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
# 1. Repair JSON file
|
|
226
|
+
if action_type == "repair_json_file" and p.is_file():
|
|
227
|
+
with open(p, "r", encoding="utf-8", errors="replace") as f:
|
|
228
|
+
content = f.read()
|
|
229
|
+
success, rep = cls.repair_json_text(content)
|
|
230
|
+
if success and rep != content:
|
|
231
|
+
diff = "".join(difflib.unified_diff(
|
|
232
|
+
content.splitlines(keepends=True),
|
|
233
|
+
rep.splitlines(keepends=True),
|
|
234
|
+
fromfile="before.json",
|
|
235
|
+
tofile="after.json"
|
|
236
|
+
))
|
|
237
|
+
if not dry_run:
|
|
238
|
+
with open(p, "w", encoding="utf-8", newline="\n") as f:
|
|
239
|
+
f.write(rep)
|
|
240
|
+
actions.append(HealAction(
|
|
241
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
242
|
+
action_type=action_type,
|
|
243
|
+
file_path=file_p,
|
|
244
|
+
description=f"Surgically repaired JSON syntax in {p.name}",
|
|
245
|
+
success=True,
|
|
246
|
+
diff=diff,
|
|
247
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
248
|
+
))
|
|
249
|
+
|
|
250
|
+
# 2. Create .gitignore
|
|
251
|
+
elif action_type == "create_gitignore":
|
|
252
|
+
entries = ["# T-Zero Autonomous Security Gitignore", "__pycache__/", "*.pyc", ".venv/", "venv/", "node_modules/", "dist/", "build/"] + cls.SENSITIVE_GITIGNORE_ENTRIES
|
|
253
|
+
content = "\n".join(entries) + "\n"
|
|
254
|
+
if not dry_run:
|
|
255
|
+
with open(p, "w", encoding="utf-8", newline="\n") as f:
|
|
256
|
+
f.write(content)
|
|
257
|
+
actions.append(HealAction(
|
|
258
|
+
category=DiagnosticCategory.SECURITY.value,
|
|
259
|
+
action_type=action_type,
|
|
260
|
+
file_path=file_p,
|
|
261
|
+
description="Generated secure .gitignore protecting .env and key files",
|
|
262
|
+
success=True,
|
|
263
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
264
|
+
))
|
|
265
|
+
|
|
266
|
+
# 3. Append to .gitignore
|
|
267
|
+
elif action_type == "append_gitignore_entries":
|
|
268
|
+
missing = issue.metadata.get("entries", [])
|
|
269
|
+
if missing:
|
|
270
|
+
append_str = "\n# T-Zero Security Guard Protections\n" + "\n".join(missing) + "\n"
|
|
271
|
+
if not dry_run:
|
|
272
|
+
with open(p, "a", encoding="utf-8") as f:
|
|
273
|
+
f.write(append_str)
|
|
274
|
+
actions.append(HealAction(
|
|
275
|
+
category=DiagnosticCategory.SECURITY.value,
|
|
276
|
+
action_type=action_type,
|
|
277
|
+
file_path=file_p,
|
|
278
|
+
description=f"Appended {len(missing)} security patterns ({', '.join(missing[:3])}) to .gitignore",
|
|
279
|
+
success=True,
|
|
280
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
281
|
+
))
|
|
282
|
+
|
|
283
|
+
# 4. Create .gitattributes
|
|
284
|
+
elif action_type == "create_gitattributes":
|
|
285
|
+
content = (
|
|
286
|
+
"# T-Zero Standard Gitattributes - Normalize LF Line Endings\n"
|
|
287
|
+
"* text=auto eol=lf\n"
|
|
288
|
+
"*.py text eol=lf\n"
|
|
289
|
+
"*.json text eol=lf\n"
|
|
290
|
+
"*.md text eol=lf\n"
|
|
291
|
+
"*.yml text eol=lf\n"
|
|
292
|
+
"*.yaml text eol=lf\n"
|
|
293
|
+
"*.sh text eol=lf\n"
|
|
294
|
+
"*.bat text eol=crlf\n"
|
|
295
|
+
"*.ps1 text eol=crlf\n"
|
|
296
|
+
)
|
|
297
|
+
if not dry_run:
|
|
298
|
+
with open(p, "w", encoding="utf-8", newline="\n") as f:
|
|
299
|
+
f.write(content)
|
|
300
|
+
actions.append(HealAction(
|
|
301
|
+
category=DiagnosticCategory.ENVIRONMENT.value,
|
|
302
|
+
action_type=action_type,
|
|
303
|
+
file_path=file_p,
|
|
304
|
+
description="Created .gitattributes to ensure cross-platform LF line ending consistency",
|
|
305
|
+
success=True,
|
|
306
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
307
|
+
))
|
|
308
|
+
except Exception as e:
|
|
309
|
+
actions.append(HealAction(
|
|
310
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
311
|
+
action_type=action_type,
|
|
312
|
+
file_path=file_p,
|
|
313
|
+
description=f"Failed configuration remediation: {e}",
|
|
314
|
+
success=False,
|
|
315
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
316
|
+
))
|
|
317
|
+
|
|
318
|
+
return actions
|