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
File without changes
@@ -0,0 +1,200 @@
1
+ """Shared static analysis utilities for TSX/TS files.
2
+
3
+ Used by ui_audit, lint, state, and i18n commands to scan React source
4
+ files for shadcn violations, import patterns, query keys, and i18n issues.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ from pathlib import Path
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # SHADCN_ELEMENT_MAP: regex pattern -> (element_name, shadcn_replacement)
15
+ # ---------------------------------------------------------------------------
16
+
17
+ SHADCN_ELEMENT_MAP: dict[str, tuple[str, str]] = {
18
+ # Basic HTML elements
19
+ r"<button[\s>]": ("button", "Button"),
20
+ r"<input[\s/>]": ("input", "Input"),
21
+ r"<select[\s>]": ("select", "Select + SelectTrigger + SelectContent + SelectItem"),
22
+ r"<textarea[\s/>]": ("textarea", "Textarea"),
23
+ r"<label[\s>]": ("label", "Label"),
24
+ r"<table[\s>]": ("table", "Table + TableHeader + TableBody + TableRow"),
25
+ r"<hr\s*/?\s*>": ("hr", "Separator"),
26
+ # Anti-patterns: title= attribute
27
+ r'\btitle\s*=\s*["\'{]': ("title= attr", "Tooltip + TooltipTrigger + TooltipContent"),
28
+ # Anti-patterns: custom modal (fixed inset-0)
29
+ r'className="[^"]*fixed\s+inset-0': ("fixed inset-0 div", "Dialog or Sheet"),
30
+ r"className='[^']*fixed\s+inset-0": ("fixed inset-0 div", "Dialog or Sheet"),
31
+ # Anti-patterns: animate-pulse skeleton
32
+ r'className="[^"]*animate-pulse': ("animate-pulse div", "Skeleton"),
33
+ r"className='[^']*animate-pulse": ("animate-pulse div", "Skeleton"),
34
+ # Anti-patterns: absolute dropdown
35
+ r'className="[^"]*absolute[^"]*top-': ("absolute dropdown div", "DropdownMenu"),
36
+ r"className='[^']*absolute[^']*top-": ("absolute dropdown div", "DropdownMenu"),
37
+ # More HTML elements
38
+ r"<img[\s/>]": ("img", "Avatar + AvatarImage + AvatarFallback (for avatars)"),
39
+ r"<progress[\s/>]": ("progress", "Progress"),
40
+ r"<details[\s>]": ("details", "Collapsible + CollapsibleTrigger + CollapsibleContent"),
41
+ r"<summary[\s>]": ("summary", "CollapsibleTrigger"),
42
+ # Anti-patterns: overflow-auto scroll area
43
+ r'className="[^"]*overflow-auto': ("overflow-auto div", "ScrollArea"),
44
+ r"className='[^']*overflow-auto": ("overflow-auto div", "ScrollArea"),
45
+ # Anti-patterns: custom toggle
46
+ r'role="switch"': ("role=switch", "Toggle or Switch"),
47
+ r'role="tab"': ("role=tab", "Tabs + TabsList + TabsTrigger + TabsContent"),
48
+ }
49
+
50
+
51
+ def find_raw_html_elements(file_path: Path) -> list[dict[str, str | int]]:
52
+ """Scan a TSX file for raw HTML that should use shadcn/ui components.
53
+
54
+ Returns a list of dicts with keys: file, line, element, replacement, code.
55
+ """
56
+ results: list[dict[str, str | int]] = []
57
+ try:
58
+ lines = file_path.read_text().splitlines()
59
+ except OSError:
60
+ return results
61
+
62
+ for line_num, line in enumerate(lines, start=1):
63
+ for pattern, (element, replacement) in SHADCN_ELEMENT_MAP.items():
64
+ if re.search(pattern, line):
65
+ results.append(
66
+ {
67
+ "file": str(file_path),
68
+ "line": line_num,
69
+ "element": element,
70
+ "replacement": replacement,
71
+ "code": line.strip(),
72
+ }
73
+ )
74
+ return results
75
+
76
+
77
+ def scan_imports(file_path: Path, package_name: str) -> list[str]:
78
+ """Extract named imports from a specific package.
79
+
80
+ Handles both single-line and multi-line import statements.
81
+ Returns a sorted list of imported names.
82
+ """
83
+ try:
84
+ text = file_path.read_text()
85
+ except OSError:
86
+ return []
87
+
88
+ # Collapse multi-line imports into single lines for easier parsing
89
+ # Match: import { ... } from "package"
90
+ escaped = re.escape(package_name)
91
+ pattern = rf'import\s*\{{([^}}]*)\}}\s*from\s*["\']{escaped}["\']'
92
+ names: list[str] = []
93
+ for match in re.finditer(pattern, text, re.DOTALL):
94
+ raw = match.group(1)
95
+ for name in raw.split(","):
96
+ name = name.strip()
97
+ if name:
98
+ # Handle "Foo as Bar" — keep the local name
99
+ parts = name.split(" as ")
100
+ names.append(parts[-1].strip())
101
+ return sorted(names)
102
+
103
+
104
+ def read_tsconfig_strict(app_dir: Path) -> bool:
105
+ """Check if tsconfig.json has ``strict: true``.
106
+
107
+ Handles JSON with ``//`` comments (common in tsconfig files).
108
+ """
109
+ tsconfig = app_dir / "tsconfig.json"
110
+ if not tsconfig.exists():
111
+ return False
112
+ try:
113
+ text = tsconfig.read_text()
114
+ # Strip single-line comments
115
+ text = re.sub(r"//.*", "", text)
116
+ data = json.loads(text)
117
+ return bool(data.get("compilerOptions", {}).get("strict", False))
118
+ except (json.JSONDecodeError, OSError):
119
+ return False
120
+
121
+
122
+ def find_query_keys(file_path: Path) -> list[dict[str, str | int]]:
123
+ """Extract TanStack Query queryKey definitions from a file.
124
+
125
+ Returns a list of dicts with keys: file, line, key_prefix, full_key.
126
+ """
127
+ results: list[dict[str, str | int]] = []
128
+ try:
129
+ lines = file_path.read_text().splitlines()
130
+ except OSError:
131
+ return results
132
+
133
+ pattern = re.compile(r"queryKey\s*:\s*\[([^\]]+)\]")
134
+
135
+ for line_num, line in enumerate(lines, start=1):
136
+ match = pattern.search(line)
137
+ if match:
138
+ full_key = match.group(1).strip()
139
+ # Extract first element as prefix (strip quotes)
140
+ parts = [p.strip().strip("\"'") for p in full_key.split(",")]
141
+ key_prefix = parts[0] if parts else ""
142
+ results.append(
143
+ {
144
+ "file": str(file_path),
145
+ "line": line_num,
146
+ "key_prefix": key_prefix,
147
+ "full_key": full_key,
148
+ }
149
+ )
150
+ return results
151
+
152
+
153
+ def find_hook_files(src_dir: Path) -> list[Path]:
154
+ """Find all TanStack Query hook files (use*.ts) in src/hooks/.
155
+
156
+ Returns sorted list of Paths matching use*.ts in the hooks directory.
157
+ """
158
+ hooks_dir = src_dir / "hooks"
159
+ if not hooks_dir.is_dir():
160
+ return []
161
+ return sorted(p for p in hooks_dir.iterdir() if p.is_file() and p.name.startswith("use") and p.suffix == ".ts")
162
+
163
+
164
+ def check_interpolation_vars(en: dict, id_: dict, prefix: str = "") -> list[dict[str, str | set[str]]]:
165
+ """Compare ``{{var}}`` placeholders between en and id translation dicts.
166
+
167
+ Recursively walks nested dicts. Returns a list of dicts with keys:
168
+ key, en_vars, id_vars, missing_in_id, extra_in_id.
169
+ """
170
+ var_pattern = re.compile(r"\{\{(\w+)\}\}")
171
+ mismatches: list[dict[str, str | set[str]]] = []
172
+
173
+ for key in en:
174
+ full_key = f"{prefix}.{key}" if prefix else key
175
+ en_val = en[key]
176
+ id_val = id_.get(key)
177
+
178
+ if isinstance(en_val, dict):
179
+ id_sub = id_val if isinstance(id_val, dict) else {}
180
+ mismatches.extend(check_interpolation_vars(en_val, id_sub, prefix=full_key))
181
+ continue
182
+
183
+ if not isinstance(en_val, str):
184
+ continue
185
+
186
+ en_vars = set(var_pattern.findall(en_val))
187
+ id_vars = set(var_pattern.findall(id_val)) if isinstance(id_val, str) else set()
188
+
189
+ if en_vars != id_vars:
190
+ mismatches.append(
191
+ {
192
+ "key": full_key,
193
+ "en_vars": en_vars,
194
+ "id_vars": id_vars,
195
+ "missing_in_id": en_vars - id_vars,
196
+ "extra_in_id": id_vars - en_vars,
197
+ }
198
+ )
199
+
200
+ return mismatches
@@ -0,0 +1,70 @@
1
+ """Typer global callback and shared context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from kctl_lib.callbacks import AppContextBase
10
+
11
+ from kctl_react.core.config import resolve_project_root
12
+ from kctl_react.core.discovery import discover_apps, discover_packages, get_app_dir as _get_app_dir
13
+
14
+
15
+ @dataclass
16
+ class AppContext(AppContextBase):
17
+ """kctl-react application context with monorepo discovery."""
18
+
19
+ root_override: str | None = None
20
+ _project_root: Path | None = field(default=None, repr=False)
21
+ _app_registry: dict[str, dict[str, Any]] | None = field(default=None, repr=False)
22
+ _package_list: list[str] | None = field(default=None, repr=False)
23
+
24
+ @property
25
+ def project_root(self) -> Path:
26
+ """Lazy-resolved monorepo root path."""
27
+ if self._project_root is None:
28
+ self._project_root = resolve_project_root(
29
+ profile_name=self.profile,
30
+ root_override=self.root_override,
31
+ )
32
+ return self._project_root
33
+
34
+ @property
35
+ def apps(self) -> dict[str, dict[str, Any]]:
36
+ """Auto-discovered app registry from apps/ directory."""
37
+ if self._app_registry is None:
38
+ self._app_registry = discover_apps(self.project_root)
39
+ return self._app_registry
40
+
41
+ @property
42
+ def app_names(self) -> list[str]:
43
+ """Sorted list of discovered app names."""
44
+ return list(self.apps.keys())
45
+
46
+ @property
47
+ def packages(self) -> list[str]:
48
+ """Auto-discovered package names from packages/ directory."""
49
+ if self._package_list is None:
50
+ self._package_list = discover_packages(self.project_root)
51
+ return self._package_list
52
+
53
+ def get_framework(self, app_name: str) -> str:
54
+ """Return framework for an app: 'vite' or 'nextjs'."""
55
+ return self.apps.get(app_name, {}).get("framework", "vite")
56
+
57
+ def is_nextjs(self, app_name: str) -> bool:
58
+ """Check if an app uses Next.js."""
59
+ return self.get_framework(app_name) == "nextjs"
60
+
61
+ def get_app_dir(self, app_name: str) -> Path:
62
+ """Get the filesystem path for an app."""
63
+ return _get_app_dir(self.project_root, app_name, self.apps)
64
+
65
+ def validate_app(self, app_name: str) -> None:
66
+ """Validate app name against discovered apps."""
67
+ if app_name not in self.apps:
68
+ from kctl_lib.exceptions import AppNotFoundError
69
+
70
+ raise AppNotFoundError(app_name, self.app_names)
@@ -0,0 +1,3 @@
1
+ """Compliance audit engine for kctl-react."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,3 @@
1
+ """API check: static schema vs hooks cross-validation."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,18 @@
1
+ """API check registry — all 6 checkers."""
2
+
3
+ from .endpoints import EndpointChecker
4
+ from .envelope import EnvelopeChecker
5
+ from .naming import NamingChecker
6
+ from .params import ParamChecker
7
+ from .requests import RequestChecker
8
+ from .types import TypeChecker
9
+
10
+ ALL_API_CHECKERS = [
11
+ EndpointChecker(),
12
+ TypeChecker(),
13
+ RequestChecker(),
14
+ ParamChecker(),
15
+ EnvelopeChecker(),
16
+ NamingChecker(),
17
+ ]
18
+ CHECKER_MAP = {c.name: c for c in ALL_API_CHECKERS}
@@ -0,0 +1,53 @@
1
+ """Endpoint coverage checker — orphan and dead endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kctl_react.core.compliance.api_check.hooks import HookCall
6
+ from kctl_react.core.compliance.api_check.matcher import MatchResult
7
+ from kctl_react.core.compliance.api_check.schema import ParsedSchema
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+
11
+ class EndpointChecker:
12
+ name = "endpoints"
13
+ label = "Endpoint Coverage"
14
+ max_points = 10
15
+
16
+ def check(
17
+ self,
18
+ schema: ParsedSchema,
19
+ hooks: list[HookCall],
20
+ match: MatchResult,
21
+ ) -> CategoryResult:
22
+ violations: list[Violation] = []
23
+
24
+ # Skip auth endpoints — handled by @kodemeio/core, not app hooks
25
+ _SKIP_PREFIXES = ("/auth/",)
26
+
27
+ for op in match.orphan_endpoints:
28
+ if any(op.path.startswith(p) for p in _SKIP_PREFIXES):
29
+ continue
30
+ violations.append(
31
+ Violation(
32
+ file="openapi.json",
33
+ message=f"Orphan endpoint: {op.method.upper()} {op.path} — no hook calls this",
34
+ fix_hint=f"Create a hook that calls {op.method.upper()} {op.path}",
35
+ )
36
+ )
37
+
38
+ for hook in match.dead_endpoints:
39
+ violations.append(
40
+ Violation(
41
+ file=hook.file,
42
+ line=hook.line,
43
+ message=f"Dead endpoint: {hook.method.upper()} {hook.normalized_url} in {hook.file} — not in OpenAPI schema",
44
+ fix_hint="Remove this API call or add the endpoint to the OpenAPI schema",
45
+ )
46
+ )
47
+
48
+ return CategoryResult(
49
+ name=self.name,
50
+ label=self.label,
51
+ max_points=self.max_points,
52
+ violations=violations,
53
+ )
@@ -0,0 +1,44 @@
1
+ """Response envelope conformance checker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kctl_react.core.compliance.api_check.hooks import HookCall
6
+ from kctl_react.core.compliance.api_check.matcher import MatchResult
7
+ from kctl_react.core.compliance.api_check.schema import ParsedSchema
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+
11
+ class EnvelopeChecker:
12
+ name = "envelope"
13
+ label = "Response Envelope"
14
+ max_points = 5
15
+
16
+ def check(
17
+ self,
18
+ schema: ParsedSchema,
19
+ hooks: list[HookCall],
20
+ match: MatchResult,
21
+ ) -> CategoryResult:
22
+ violations: list[Violation] = []
23
+
24
+ for name, schema_def in schema.response_schemas.items():
25
+ # Only check schemas that are response envelopes (end with Response)
26
+ if not name.endswith("Response"):
27
+ continue
28
+ properties = schema_def.get("properties", {})
29
+ for required_key in ("success", "data"):
30
+ if required_key not in properties:
31
+ violations.append(
32
+ Violation(
33
+ file="openapi.json",
34
+ message=f"Schema {name} missing {required_key} in response envelope",
35
+ fix_hint=f"Add '{required_key}' property to {name}",
36
+ )
37
+ )
38
+
39
+ return CategoryResult(
40
+ name=self.name,
41
+ label=self.label,
42
+ max_points=self.max_points,
43
+ violations=violations,
44
+ )
@@ -0,0 +1,60 @@
1
+ """Hook naming convention checker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from kctl_react.core.compliance.api_check.hooks import HookCall
8
+ from kctl_react.core.compliance.api_check.matcher import MatchResult
9
+ from kctl_react.core.compliance.api_check.schema import ParsedSchema
10
+ from kctl_react.core.compliance.models import CategoryResult, Violation
11
+
12
+ _USE_PREFIX_RE = re.compile(r"use([A-Z]\w*)\.ts$")
13
+
14
+
15
+ class NamingChecker:
16
+ name = "naming"
17
+ label = "Hook Naming"
18
+ max_points = 5
19
+
20
+ def check(
21
+ self,
22
+ schema: ParsedSchema,
23
+ hooks: list[HookCall],
24
+ match: MatchResult,
25
+ ) -> CategoryResult:
26
+ violations: list[Violation] = []
27
+
28
+ # Collect all tags from the schema
29
+ tags = {op.tag.lower() for op in schema.operations}
30
+
31
+ # Group hooks by file to avoid duplicate violations
32
+ seen_files: set[str] = set()
33
+ for hook in hooks:
34
+ if hook.file in seen_files:
35
+ continue
36
+ seen_files.add(hook.file)
37
+
38
+ # Extract resource name from filename
39
+ filename = hook.file.rsplit("/", 1)[-1] if "/" in hook.file else hook.file
40
+ m = _USE_PREFIX_RE.match(filename)
41
+ if not m:
42
+ continue
43
+
44
+ resource = m.group(1).lower()
45
+
46
+ if not any(resource in tag for tag in tags):
47
+ violations.append(
48
+ Violation(
49
+ file=hook.file,
50
+ message=f"Hook {hook.file} name doesn't match any OpenAPI tag",
51
+ fix_hint="Rename the hook file to match a tag in the OpenAPI schema",
52
+ )
53
+ )
54
+
55
+ return CategoryResult(
56
+ name=self.name,
57
+ label=self.label,
58
+ max_points=self.max_points,
59
+ violations=violations,
60
+ )
@@ -0,0 +1,44 @@
1
+ """Query parameter validation checker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kctl_react.core.compliance.api_check.hooks import HookCall
6
+ from kctl_react.core.compliance.api_check.matcher import MatchResult
7
+ from kctl_react.core.compliance.api_check.schema import ParsedSchema
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+
11
+ class ParamChecker:
12
+ name = "params"
13
+ label = "Query Parameters"
14
+ max_points = 5
15
+
16
+ def check(
17
+ self,
18
+ schema: ParsedSchema,
19
+ hooks: list[HookCall],
20
+ match: MatchResult,
21
+ ) -> CategoryResult:
22
+ violations: list[Violation] = []
23
+
24
+ for hook, operation in match.matched:
25
+ for param in hook.query_params:
26
+ if param not in operation.parameters:
27
+ violations.append(
28
+ Violation(
29
+ file=hook.file,
30
+ line=hook.line,
31
+ message=(
32
+ f"Unknown query param '{param}' on "
33
+ f"{hook.method.upper()} {hook.normalized_url} in {hook.file}"
34
+ ),
35
+ fix_hint=f"Remove '{param}' or add it to the OpenAPI schema",
36
+ )
37
+ )
38
+
39
+ return CategoryResult(
40
+ name=self.name,
41
+ label=self.label,
42
+ max_points=self.max_points,
43
+ violations=violations,
44
+ )
@@ -0,0 +1,57 @@
1
+ """Request type matching checker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kctl_react.core.compliance.api_check.hooks import HookCall
6
+ from kctl_react.core.compliance.api_check.matcher import MatchResult
7
+ from kctl_react.core.compliance.api_check.schema import ParsedSchema
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+
11
+ class RequestChecker:
12
+ name = "requests"
13
+ label = "Request Types"
14
+ max_points = 5
15
+
16
+ def check(
17
+ self,
18
+ schema: ParsedSchema,
19
+ hooks: list[HookCall],
20
+ match: MatchResult,
21
+ ) -> CategoryResult:
22
+ violations: list[Violation] = []
23
+
24
+ for hook, operation in match.matched:
25
+ if operation.method not in ("post", "put", "patch"):
26
+ continue
27
+
28
+ if operation.request_schema and hook.request_type is None:
29
+ violations.append(
30
+ Violation(
31
+ file=hook.file,
32
+ line=hook.line,
33
+ message=(
34
+ f"Missing request body type on {hook.method.upper()} {hook.normalized_url} in {hook.file}"
35
+ ),
36
+ fix_hint=f"Add request type {operation.request_schema}",
37
+ )
38
+ )
39
+ elif hook.request_type and operation.request_schema and hook.request_type != operation.request_schema:
40
+ violations.append(
41
+ Violation(
42
+ file=hook.file,
43
+ line=hook.line,
44
+ message=(
45
+ f"Request type mismatch: hook uses {hook.request_type} "
46
+ f"but schema expects {operation.request_schema}"
47
+ ),
48
+ fix_hint=f"Change request type to {operation.request_schema}",
49
+ )
50
+ )
51
+
52
+ return CategoryResult(
53
+ name=self.name,
54
+ label=self.label,
55
+ max_points=self.max_points,
56
+ violations=violations,
57
+ )
@@ -0,0 +1,55 @@
1
+ """Response type matching checker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kctl_react.core.compliance.api_check.hooks import HookCall
6
+ from kctl_react.core.compliance.api_check.matcher import MatchResult
7
+ from kctl_react.core.compliance.api_check.schema import ParsedSchema
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+
11
+ class TypeChecker:
12
+ name = "types"
13
+ label = "Response Types"
14
+ max_points = 10
15
+
16
+ def check(
17
+ self,
18
+ schema: ParsedSchema,
19
+ hooks: list[HookCall],
20
+ match: MatchResult,
21
+ ) -> CategoryResult:
22
+ violations: list[Violation] = []
23
+
24
+ for hook, operation in match.matched:
25
+ if hook.response_type is None:
26
+ violations.append(
27
+ Violation(
28
+ file=hook.file,
29
+ line=hook.line,
30
+ message=(
31
+ f"Missing response type generic on "
32
+ f"{hook.method.upper()} {hook.normalized_url} in {hook.file}"
33
+ ),
34
+ fix_hint="Add a response type generic to the API call",
35
+ )
36
+ )
37
+ elif operation.response_schema and hook.response_type != operation.response_schema:
38
+ violations.append(
39
+ Violation(
40
+ file=hook.file,
41
+ line=hook.line,
42
+ message=(
43
+ f"Type mismatch: hook uses {hook.response_type} "
44
+ f"but schema expects {operation.response_schema}"
45
+ ),
46
+ fix_hint=f"Change response type to {operation.response_schema}",
47
+ )
48
+ )
49
+
50
+ return CategoryResult(
51
+ name=self.name,
52
+ label=self.label,
53
+ max_points=self.max_points,
54
+ violations=violations,
55
+ )