kctl-react 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. kctl_react/__init__.py +3 -0
  2. kctl_react/__main__.py +5 -0
  3. kctl_react/cli.py +201 -0
  4. kctl_react/commands/__init__.py +0 -0
  5. kctl_react/commands/a11y.py +78 -0
  6. kctl_react/commands/affected.py +170 -0
  7. kctl_react/commands/apps.py +353 -0
  8. kctl_react/commands/build.py +376 -0
  9. kctl_react/commands/bundle_cmd.py +217 -0
  10. kctl_react/commands/cap.py +1465 -0
  11. kctl_react/commands/clean.py +76 -0
  12. kctl_react/commands/codegen.py +491 -0
  13. kctl_react/commands/compliance.py +587 -0
  14. kctl_react/commands/config_cmd.py +368 -0
  15. kctl_react/commands/dashboard.py +163 -0
  16. kctl_react/commands/deploy.py +318 -0
  17. kctl_react/commands/deps.py +792 -0
  18. kctl_react/commands/dev.py +96 -0
  19. kctl_react/commands/docker_cmd.py +73 -0
  20. kctl_react/commands/doctor.py +170 -0
  21. kctl_react/commands/e2e.py +343 -0
  22. kctl_react/commands/env.py +155 -0
  23. kctl_react/commands/i18n.py +310 -0
  24. kctl_react/commands/lint.py +306 -0
  25. kctl_react/commands/maintenance.py +308 -0
  26. kctl_react/commands/monitor_cmd.py +50 -0
  27. kctl_react/commands/observe.py +34 -0
  28. kctl_react/commands/packages.py +129 -0
  29. kctl_react/commands/perf.py +762 -0
  30. kctl_react/commands/pipeline.py +289 -0
  31. kctl_react/commands/pwa.py +193 -0
  32. kctl_react/commands/scaffold.py +323 -0
  33. kctl_react/commands/security.py +660 -0
  34. kctl_react/commands/skill_cmd.py +54 -0
  35. kctl_react/commands/state.py +254 -0
  36. kctl_react/commands/test_cmd.py +418 -0
  37. kctl_react/commands/ui_audit.py +889 -0
  38. kctl_react/core/__init__.py +0 -0
  39. kctl_react/core/analyzers.py +200 -0
  40. kctl_react/core/callbacks.py +70 -0
  41. kctl_react/core/compliance/__init__.py +3 -0
  42. kctl_react/core/compliance/api_check/__init__.py +3 -0
  43. kctl_react/core/compliance/api_check/checks/__init__.py +18 -0
  44. kctl_react/core/compliance/api_check/checks/endpoints.py +53 -0
  45. kctl_react/core/compliance/api_check/checks/envelope.py +44 -0
  46. kctl_react/core/compliance/api_check/checks/naming.py +60 -0
  47. kctl_react/core/compliance/api_check/checks/params.py +44 -0
  48. kctl_react/core/compliance/api_check/checks/requests.py +57 -0
  49. kctl_react/core/compliance/api_check/checks/types.py +55 -0
  50. kctl_react/core/compliance/api_check/hooks.py +133 -0
  51. kctl_react/core/compliance/api_check/matcher.py +55 -0
  52. kctl_react/core/compliance/api_check/schema.py +151 -0
  53. kctl_react/core/compliance/api_health/__init__.py +35 -0
  54. kctl_react/core/compliance/api_health/checks/__init__.py +9 -0
  55. kctl_react/core/compliance/api_health/checks/auth.py +72 -0
  56. kctl_react/core/compliance/api_health/checks/reachable.py +44 -0
  57. kctl_react/core/compliance/api_health/checks/response.py +55 -0
  58. kctl_react/core/compliance/api_health/checks/timing.py +38 -0
  59. kctl_react/core/compliance/api_health/client.py +99 -0
  60. kctl_react/core/compliance/api_health/sampler.py +16 -0
  61. kctl_react/core/compliance/checks/__init__.py +47 -0
  62. kctl_react/core/compliance/checks/api.py +101 -0
  63. kctl_react/core/compliance/checks/codegen.py +94 -0
  64. kctl_react/core/compliance/checks/darkmode.py +57 -0
  65. kctl_react/core/compliance/checks/errors.py +68 -0
  66. kctl_react/core/compliance/checks/features.py +66 -0
  67. kctl_react/core/compliance/checks/i18n_check.py +105 -0
  68. kctl_react/core/compliance/checks/imports.py +86 -0
  69. kctl_react/core/compliance/checks/navigation.py +62 -0
  70. kctl_react/core/compliance/checks/practices.py +122 -0
  71. kctl_react/core/compliance/checks/providers.py +85 -0
  72. kctl_react/core/compliance/checks/pwa.py +101 -0
  73. kctl_react/core/compliance/checks/responsive.py +47 -0
  74. kctl_react/core/compliance/checks/scripts.py +85 -0
  75. kctl_react/core/compliance/checks/shadcn.py +51 -0
  76. kctl_react/core/compliance/checks/structure.py +76 -0
  77. kctl_react/core/compliance/checks/testing.py +83 -0
  78. kctl_react/core/compliance/checks/theme.py +92 -0
  79. kctl_react/core/compliance/checks/ui_standard.py +185 -0
  80. kctl_react/core/compliance/checks/vite.py +83 -0
  81. kctl_react/core/compliance/engine.py +87 -0
  82. kctl_react/core/compliance/exceptions_map.py +15 -0
  83. kctl_react/core/compliance/fixes/__init__.py +33 -0
  84. kctl_react/core/compliance/fixes/codegen_fix.py +28 -0
  85. kctl_react/core/compliance/fixes/i18n_fix.py +61 -0
  86. kctl_react/core/compliance/fixes/imports_fix.py +36 -0
  87. kctl_react/core/compliance/fixes/structure_fix.py +20 -0
  88. kctl_react/core/compliance/fixes/theme_fix.py +29 -0
  89. kctl_react/core/compliance/models.py +106 -0
  90. kctl_react/core/config.py +201 -0
  91. kctl_react/core/discovery.py +185 -0
  92. kctl_react/core/exceptions.py +17 -0
  93. kctl_react/core/git.py +146 -0
  94. kctl_react/core/history.py +121 -0
  95. kctl_react/core/output.py +5 -0
  96. kctl_react/core/plugins.py +13 -0
  97. kctl_react/core/runner.py +34 -0
  98. kctl_react/py.typed +0 -0
  99. kctl_react-0.6.2.dist-info/METADATA +17 -0
  100. kctl_react-0.6.2.dist-info/RECORD +102 -0
  101. kctl_react-0.6.2.dist-info/WHEEL +4 -0
  102. kctl_react-0.6.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,76 @@
1
+ """Checker: project file/directory structure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from kctl_react.core.compliance.models import CategoryResult, Violation
8
+
9
+ REQUIRED_DIRS = [
10
+ "api",
11
+ "pages",
12
+ "components",
13
+ "config",
14
+ "hooks",
15
+ "contexts",
16
+ "constants",
17
+ "types",
18
+ "i18n",
19
+ "utils",
20
+ "__tests__",
21
+ ]
22
+
23
+ REQUIRED_FILES = [
24
+ "main.tsx",
25
+ "App.tsx",
26
+ "config/navConfig.tsx",
27
+ "components/AppLayout.tsx",
28
+ "api/client.ts",
29
+ "types/api.ts",
30
+ ]
31
+
32
+
33
+ class StructureChecker:
34
+ name = "structure"
35
+ label = "File Structure"
36
+ max_points = 8
37
+
38
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
39
+ violations: list[Violation] = []
40
+ src = app_path / "src"
41
+
42
+ for d in REQUIRED_DIRS:
43
+ if not (src / d).is_dir():
44
+ violations.append(
45
+ Violation(
46
+ file=f"src/{d}/",
47
+ message=f"Missing required directory: src/{d}/",
48
+ fix_hint=f"mkdir -p apps/{app_name}/src/{d}",
49
+ )
50
+ )
51
+
52
+ for f in REQUIRED_FILES:
53
+ if not (src / f).is_file():
54
+ violations.append(
55
+ Violation(
56
+ file=f"src/{f}",
57
+ message=f"Missing required file: src/{f}",
58
+ )
59
+ )
60
+
61
+ # .env.example in app root
62
+ if not (app_path / ".env.example").is_file():
63
+ violations.append(
64
+ Violation(
65
+ file=".env.example",
66
+ message="Missing .env.example in app root",
67
+ fix_hint="Create .env.example with sanitized env vars",
68
+ )
69
+ )
70
+
71
+ return CategoryResult(
72
+ name=self.name,
73
+ label=self.label,
74
+ max_points=self.max_points,
75
+ violations=violations,
76
+ )
@@ -0,0 +1,83 @@
1
+ """Checker: testing setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+
11
+ class TestingChecker:
12
+ name = "testing"
13
+ label = "Testing Setup"
14
+ max_points = 4
15
+
16
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
17
+ violations: list[Violation] = []
18
+ src = app_path / "src"
19
+
20
+ # __tests__/ directory with test files
21
+ tests_dir = src / "__tests__"
22
+ if tests_dir.is_dir():
23
+ test_files = list(tests_dir.rglob("*.test.tsx")) + list(tests_dir.rglob("*.test.ts"))
24
+ if not test_files:
25
+ violations.append(
26
+ Violation(
27
+ file="src/__tests__/",
28
+ message="No test files found in __tests__/",
29
+ fix_hint="Add .test.tsx/.test.ts files",
30
+ )
31
+ )
32
+ else:
33
+ violations.append(
34
+ Violation(
35
+ file="src/__tests__/",
36
+ message="Missing __tests__/ directory",
37
+ fix_hint="Create src/__tests__/ with test files",
38
+ )
39
+ )
40
+
41
+ # package.json test scripts
42
+ pkg = app_path / "package.json"
43
+ if pkg.is_file():
44
+ try:
45
+ data = json.loads(pkg.read_text())
46
+ except (json.JSONDecodeError, OSError):
47
+ data = {}
48
+ scripts = data.get("scripts", {})
49
+ for s in ("test", "test:ci"):
50
+ if s not in scripts:
51
+ violations.append(
52
+ Violation(
53
+ file="package.json",
54
+ message=f'Missing script: "{s}"',
55
+ fix_hint=f"Add {s} script to package.json",
56
+ )
57
+ )
58
+
59
+ # vitest config
60
+ vitest_config = app_path / "vitest.config.ts"
61
+ vite_config = app_path / "vite.config.ts"
62
+ has_vitest_config = vitest_config.is_file()
63
+ if not has_vitest_config and vite_config.is_file():
64
+ content = vite_config.read_text()
65
+ has_vitest_config = (
66
+ ("test" in content and "vitest" in content.lower()) or "test:" in content or "test :" in content
67
+ )
68
+
69
+ if not has_vitest_config:
70
+ violations.append(
71
+ Violation(
72
+ file="vitest.config.ts",
73
+ message="No vitest configuration found",
74
+ fix_hint="Add vitest.config.ts or test config in vite.config.ts",
75
+ )
76
+ )
77
+
78
+ return CategoryResult(
79
+ name=self.name,
80
+ label=self.label,
81
+ max_points=self.max_points,
82
+ violations=violations,
83
+ )
@@ -0,0 +1,92 @@
1
+ """Checker: theme setup and color token isolation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+ COLOR_TOKEN_PATTERN = re.compile(r"--(?:primary|secondary|accent|destructive)\s*:")
11
+
12
+
13
+ class ThemeChecker:
14
+ name = "theme"
15
+ label = "Theme Setup"
16
+ max_points = 6
17
+
18
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
19
+ violations: list[Violation] = []
20
+ src = app_path / "src"
21
+
22
+ # index.css should import themes/{app}.css
23
+ index_css = src / "index.css"
24
+ if index_css.is_file():
25
+ content = index_css.read_text()
26
+ expected_import = f"themes/{app_name}.css"
27
+ if expected_import not in content:
28
+ violations.append(
29
+ Violation(
30
+ file="src/index.css",
31
+ message=f"index.css does not import {expected_import}",
32
+ fix_hint=f'Add @import "{expected_import}" to index.css',
33
+ )
34
+ )
35
+
36
+ # No color tokens in index.css
37
+ for i, line in enumerate(content.splitlines(), 1):
38
+ if COLOR_TOKEN_PATTERN.search(line):
39
+ violations.append(
40
+ Violation(
41
+ file="src/index.css",
42
+ line=i,
43
+ message="Color token in index.css — should be in theme file",
44
+ fix_hint="Move color tokens to themes/{app}.css",
45
+ )
46
+ )
47
+ else:
48
+ violations.append(Violation(file="src/index.css", message="Missing index.css"))
49
+
50
+ # Check globals.css for color tokens too
51
+ globals_css = src / "globals.css"
52
+ if globals_css.is_file():
53
+ content = globals_css.read_text()
54
+ for i, line in enumerate(content.splitlines(), 1):
55
+ if COLOR_TOKEN_PATTERN.search(line):
56
+ violations.append(
57
+ Violation(
58
+ file="src/globals.css",
59
+ line=i,
60
+ message="Color token in globals.css — should be in theme file",
61
+ fix_hint="Move color tokens to themes/{app}.css",
62
+ )
63
+ )
64
+
65
+ # ThemeToggle in AppLayout.tsx
66
+ layout = src / "components" / "AppLayout.tsx"
67
+ if layout.is_file():
68
+ content = layout.read_text()
69
+ if "ThemeToggle" not in content:
70
+ violations.append(
71
+ Violation(
72
+ file="src/components/AppLayout.tsx",
73
+ message="Missing ThemeToggle in AppLayout",
74
+ fix_hint="Add ThemeToggle from @kodemeio/ui",
75
+ )
76
+ )
77
+ # Check app-specific storageKey
78
+ if "storageKey" not in content and "ThemeToggle" in content:
79
+ violations.append(
80
+ Violation(
81
+ file="src/components/AppLayout.tsx",
82
+ message="ThemeToggle missing app-specific storageKey",
83
+ fix_hint=f'Add storageKey="{app_name}_theme"',
84
+ )
85
+ )
86
+
87
+ return CategoryResult(
88
+ name=self.name,
89
+ label=self.label,
90
+ max_points=self.max_points,
91
+ violations=violations,
92
+ )
@@ -0,0 +1,185 @@
1
+ """Checker: UI standardization — shared component usage and import consistency."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+ # Components that should NOT exist locally — they are provided by @kodemeio/ui
11
+ SHARED_COMPONENTS = {
12
+ "Button.tsx",
13
+ "Input.tsx",
14
+ "Textarea.tsx",
15
+ "Label.tsx",
16
+ "Select.tsx",
17
+ "Badge.tsx",
18
+ "Card.tsx",
19
+ "Dialog.tsx",
20
+ "Sheet.tsx",
21
+ "Alert.tsx",
22
+ "Table.tsx",
23
+ "Tabs.tsx",
24
+ "Tooltip.tsx",
25
+ "Skeleton.tsx",
26
+ "Progress.tsx",
27
+ "Separator.tsx",
28
+ "ScrollArea.tsx",
29
+ "Checkbox.tsx",
30
+ "Switch.tsx",
31
+ "DropdownMenu.tsx",
32
+ "Popover.tsx",
33
+ "RadioGroup.tsx",
34
+ }
35
+
36
+ # Shared components every app should import from @kodemeio/ui
37
+ REQUIRED_SHARED = ["PageHeader", "LoadingSpinner", "Skeleton", "EmptyState"]
38
+
39
+ # Pattern for relative path imports to packages/ui
40
+ RELATIVE_UI_IMPORT = re.compile(r"""from\s+["']\.{1,3}/.*packages/ui""")
41
+
42
+
43
+ class UiStandardChecker:
44
+ name = "ui-standard"
45
+ label = "UI Standardization"
46
+ max_points = 6
47
+
48
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
49
+ violations: list[Violation] = []
50
+ src = app_path / "src"
51
+
52
+ # 1. Duplicate components
53
+ components_dir = src / "components"
54
+ if components_dir.is_dir():
55
+ for f in components_dir.rglob("*.tsx"):
56
+ if f.name in SHARED_COMPONENTS:
57
+ rel = str(f.relative_to(app_path))
58
+ violations.append(
59
+ Violation(
60
+ file=rel,
61
+ message=f"Local duplicate of @kodemeio/ui component: {f.name}",
62
+ auto_fixable=False,
63
+ )
64
+ )
65
+
66
+ # Collect all tsx files for subsequent checks
67
+ all_tsx: list[Path] = []
68
+ if src.is_dir():
69
+ all_tsx = list(src.rglob("*.tsx"))
70
+ all_tsx = [f for f in all_tsx if "__tests__" not in f.parts]
71
+
72
+ all_contents: dict[Path, str] = {}
73
+ for f in all_tsx:
74
+ try:
75
+ all_contents[f] = f.read_text()
76
+ except OSError:
77
+ continue
78
+
79
+ # 2. Required shared component imports
80
+ # Check LoadingSpinner OR Skeleton — either counts
81
+ found_page_header = False
82
+ found_loading = False
83
+ found_empty_state = False
84
+
85
+ for content in all_contents.values():
86
+ if "@kodemeio/ui" in content:
87
+ if "PageHeader" in content:
88
+ found_page_header = True
89
+ if "LoadingSpinner" in content or "Skeleton" in content:
90
+ found_loading = True
91
+ if "EmptyState" in content:
92
+ found_empty_state = True
93
+
94
+ if not found_page_header:
95
+ violations.append(
96
+ Violation(
97
+ file="src/",
98
+ message="No usage of shared PageHeader from @kodemeio/ui",
99
+ fix_hint="Import PageHeader from @kodemeio/ui for consistent UX",
100
+ )
101
+ )
102
+ if not found_loading:
103
+ violations.append(
104
+ Violation(
105
+ file="src/",
106
+ message="No usage of shared LoadingSpinner or Skeleton from @kodemeio/ui",
107
+ fix_hint="Import LoadingSpinner or Skeleton from @kodemeio/ui for consistent UX",
108
+ )
109
+ )
110
+ if not found_empty_state:
111
+ violations.append(
112
+ Violation(
113
+ file="src/",
114
+ message="No usage of shared EmptyState from @kodemeio/ui",
115
+ fix_hint="Import EmptyState from @kodemeio/ui for consistent UX",
116
+ )
117
+ )
118
+
119
+ # 3. Badge wrapper pattern
120
+ if components_dir.is_dir():
121
+ for f in components_dir.rglob("*.tsx"):
122
+ name = f.stem # e.g. "StatusBadge"
123
+ if name.endswith("Badge") or name.endswith("StatusBadge"):
124
+ if f.name in SHARED_COMPONENTS:
125
+ continue # already caught by check 1
126
+ try:
127
+ content = f.read_text()
128
+ except OSError:
129
+ continue
130
+ if "Badge" not in content or "@kodemeio/ui" not in content:
131
+ rel = str(f.relative_to(app_path))
132
+ violations.append(
133
+ Violation(
134
+ file=rel,
135
+ message=f"{f.name} should use Badge from @kodemeio/ui as base",
136
+ )
137
+ )
138
+
139
+ # 4. DataTable usage
140
+ table_page_count = 0
141
+ has_datatable = False
142
+ pages_dir = src / "pages"
143
+
144
+ for f, content in all_contents.items():
145
+ if "@kodemeio/ui" in content and "DataTable" in content:
146
+ has_datatable = True
147
+
148
+ if pages_dir.is_dir():
149
+ for f in pages_dir.rglob("*.tsx"):
150
+ if "__tests__" in f.parts:
151
+ continue
152
+ content = all_contents.get(f, "")
153
+ if "<Table" in content or "<table" in content:
154
+ table_page_count += 1
155
+
156
+ if table_page_count >= 3 and not has_datatable:
157
+ violations.append(
158
+ Violation(
159
+ file="src/pages/",
160
+ message=f"App has {table_page_count} table pages but doesn't use DataTable from @kodemeio/ui",
161
+ fix_hint="Use DataTable from @kodemeio/ui for consistent table behavior",
162
+ )
163
+ )
164
+
165
+ # 5. Import source consistency — relative path imports to packages/ui
166
+ for f, content in all_contents.items():
167
+ lines = content.splitlines()
168
+ rel = str(f.relative_to(app_path))
169
+ for i, line in enumerate(lines, 1):
170
+ if RELATIVE_UI_IMPORT.search(line):
171
+ violations.append(
172
+ Violation(
173
+ file=rel,
174
+ line=i,
175
+ message="Direct path import to packages/ui — use @kodemeio/ui",
176
+ auto_fixable=True,
177
+ )
178
+ )
179
+
180
+ return CategoryResult(
181
+ name=self.name,
182
+ label=self.label,
183
+ max_points=self.max_points,
184
+ violations=violations,
185
+ )
@@ -0,0 +1,83 @@
1
+ """Checker: Vite configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from kctl_react.core.compliance.models import CategoryResult, Violation
8
+
9
+ # Expected ports per app
10
+ APP_PORTS: dict[str, str] = {
11
+ "sfa": "4004",
12
+ "lfa": "4005",
13
+ "shop": "4006",
14
+ "wms": "4007",
15
+ "bia": "4008",
16
+ "eam": "4009",
17
+ "mrp": "4010",
18
+ "hrm": "4011",
19
+ "tpm": "4012",
20
+ "dms": "4013",
21
+ "saas": "4014",
22
+ }
23
+
24
+
25
+ class ViteChecker:
26
+ name = "vite"
27
+ label = "Vite Config"
28
+ max_points = 4
29
+
30
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
31
+ violations: list[Violation] = []
32
+
33
+ vite_config = app_path / "vite.config.ts"
34
+ if not vite_config.is_file():
35
+ violations.append(
36
+ Violation(
37
+ file="vite.config.ts",
38
+ message="Missing vite.config.ts",
39
+ )
40
+ )
41
+ return CategoryResult(
42
+ name=self.name,
43
+ label=self.label,
44
+ max_points=self.max_points,
45
+ violations=violations,
46
+ )
47
+
48
+ content = vite_config.read_text()
49
+
50
+ if "createViteConfig" not in content:
51
+ violations.append(
52
+ Violation(
53
+ file="vite.config.ts",
54
+ message="Does not use createViteConfig from @kodemeio/vite-config",
55
+ fix_hint="Use createViteConfig() factory",
56
+ )
57
+ )
58
+
59
+ if "@kodemeio/vite-config" not in content:
60
+ violations.append(
61
+ Violation(
62
+ file="vite.config.ts",
63
+ message="Does not import from @kodemeio/vite-config",
64
+ fix_hint="Import createViteConfig from @kodemeio/vite-config",
65
+ )
66
+ )
67
+
68
+ expected_port = APP_PORTS.get(app_name)
69
+ if expected_port and expected_port not in content:
70
+ violations.append(
71
+ Violation(
72
+ file="vite.config.ts",
73
+ message=f"Port {expected_port} not configured for {app_name}",
74
+ fix_hint=f"Set port to {expected_port} in vite config",
75
+ )
76
+ )
77
+
78
+ return CategoryResult(
79
+ name=self.name,
80
+ label=self.label,
81
+ max_points=self.max_points,
82
+ violations=violations,
83
+ )
@@ -0,0 +1,87 @@
1
+ """ComplianceEngine — runs checkers and generates reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from kctl_react.core.compliance.checks import CHECKER_MAP
8
+ from kctl_react.core.compliance.models import AppReport, CategoryResult
9
+
10
+
11
+ class ComplianceEngine:
12
+ """Runs compliance checkers and generates fix prompts."""
13
+
14
+ def audit(
15
+ self,
16
+ app_path: Path,
17
+ app_name: str,
18
+ categories: list[str] | None = None,
19
+ ) -> AppReport:
20
+ """Run checkers (filtered by categories if given), return AppReport."""
21
+ report = AppReport(app=app_name)
22
+
23
+ if categories:
24
+ checkers = [(name, fn) for name, fn in CHECKER_MAP.items() if name in categories]
25
+ else:
26
+ checkers = list(CHECKER_MAP.items())
27
+
28
+ for _name, checker_fn in checkers:
29
+ result: CategoryResult = checker_fn(app_path, app_name)
30
+ report.categories.append(result)
31
+
32
+ return report
33
+
34
+ def generate_prompt(self, report: AppReport) -> str:
35
+ """Generate markdown prompt for AI/human fixing."""
36
+ pct = int(report.total_score / report.max_score * 100) if report.max_score > 0 else 100
37
+
38
+ lines: list[str] = [
39
+ f"# Fix Compliance Issues for `{report.app}`",
40
+ "",
41
+ f"- **App path**: `apps/{report.app}`",
42
+ f"- **Score**: {report.total_score}/{report.max_score} ({pct}%)",
43
+ f"- **Grade**: {report.grade}",
44
+ f"- **Total issues**: {report.total_violations}",
45
+ "",
46
+ ]
47
+
48
+ # Auto-fixable section
49
+ auto_violations = [(c, v) for c in report.categories for v in c.violations if v.auto_fixable]
50
+ lines.append("## Auto-fixable (run first)")
51
+ lines.append("")
52
+ if auto_violations:
53
+ lines.append(f"```bash\nkctl-react compliance fix {report.app}\n```")
54
+ lines.append("")
55
+ for cat, v in auto_violations:
56
+ hint = f" -- {v.fix_hint}" if v.fix_hint else ""
57
+ lines.append(f"- `{v.file}`:{v.line or '?'} {v.message}{hint}")
58
+ else:
59
+ lines.append("No auto-fixable issues found.")
60
+ lines.append("")
61
+
62
+ # Manual review section
63
+ manual_violations = [(c, v) for c in report.categories for v in c.violations if not v.auto_fixable]
64
+ lines.append("## Issues Requiring Review")
65
+ lines.append("")
66
+ if manual_violations:
67
+ current_cat = ""
68
+ for cat, v in manual_violations:
69
+ if cat.label != current_cat:
70
+ current_cat = cat.label
71
+ lines.append(f"### {current_cat}")
72
+ lines.append("")
73
+ loc = f":{v.line}" if v.line else ""
74
+ hint = f"\n - Hint: {v.fix_hint}" if v.fix_hint else ""
75
+ lines.append(f"- `{v.file}`{loc} {v.message}{hint}")
76
+ lines.append("")
77
+ else:
78
+ lines.append("No manual review issues found.")
79
+ lines.append("")
80
+
81
+ # Verification section
82
+ lines.append("## Verification")
83
+ lines.append("")
84
+ lines.append(f"```bash\nkctl-react compliance audit {report.app}\n```")
85
+ lines.append("")
86
+
87
+ return "\n".join(lines)
@@ -0,0 +1,15 @@
1
+ """App-specific exceptions for compliance checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ APP_EXCEPTIONS: dict[str, dict[str, list[str]]] = {
6
+ "mrp": {"skip_providers": ["GPSProvider"]},
7
+ "tpm": {"skip_providers": ["GPSProvider"]},
8
+ "saas": {"skip_providers": ["GPSProvider"]},
9
+ "shop": {"skip_providers": ["GPSProvider"]},
10
+ }
11
+
12
+
13
+ def get_skip_providers(app_name: str) -> list[str]:
14
+ """Return list of providers to skip for the given app."""
15
+ return APP_EXCEPTIONS.get(app_name, {}).get("skip_providers", [])
@@ -0,0 +1,33 @@
1
+ """Auto-fixers registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from kctl_react.core.compliance.fixes.codegen_fix import fix_codegen
8
+ from kctl_react.core.compliance.fixes.i18n_fix import fix_i18n
9
+ from kctl_react.core.compliance.fixes.imports_fix import fix_imports
10
+ from kctl_react.core.compliance.fixes.structure_fix import fix_structure
11
+ from kctl_react.core.compliance.fixes.theme_fix import fix_theme
12
+
13
+ ALL_FIXERS: dict = {
14
+ "structure": fix_structure,
15
+ "imports": fix_imports,
16
+ "i18n": fix_i18n,
17
+ "codegen": fix_codegen,
18
+ "theme": fix_theme,
19
+ }
20
+
21
+
22
+ def apply_fixes(
23
+ app_path: Path,
24
+ app_name: str,
25
+ dry_run: bool = False,
26
+ categories: list[str] | None = None,
27
+ ) -> int:
28
+ """Apply auto-fixes and return count of fixes applied."""
29
+ fixers = {k: v for k, v in ALL_FIXERS.items() if not categories or k in categories}
30
+ total = 0
31
+ for fix_fn in fixers.values():
32
+ total += fix_fn(app_path, app_name, dry_run=dry_run)
33
+ return total
@@ -0,0 +1,28 @@
1
+ """Fixer: create openapi-ts.config.ts if missing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ _TEMPLATE = """\
8
+ import { defineConfig } from "@hey-api/openapi-ts";
9
+
10
+ export default defineConfig({
11
+ client: "@hey-api/client-fetch",
12
+ input: "openapi.json",
13
+ output: "src/generated",
14
+ services: false,
15
+ schemas: false,
16
+ });
17
+ """
18
+
19
+
20
+ def fix_codegen(app_path: Path, app_name: str, dry_run: bool = False) -> int:
21
+ """Create openapi-ts.config.ts if missing. Return count of fixes applied."""
22
+ config_file = app_path / "openapi-ts.config.ts"
23
+ if config_file.is_file():
24
+ return 0
25
+
26
+ if not dry_run:
27
+ config_file.write_text(_TEMPLATE, encoding="utf-8")
28
+ return 1