enigma-api-client 0.3.1__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.
- enigma_api_client/__init__.py +16 -0
- enigma_api_client/client.py +134 -0
- enigma_api_client/client_inject_id_and_typename_helper.py +248 -0
- enigma_api_client/client_inject_urls_helper.py +26 -0
- enigma_api_client/plugins/.DS_Store +0 -0
- enigma_api_client/plugins/enigma-api/.claude-plugin/plugin.json +8 -0
- enigma_api_client/plugins/enigma-api/README.md +82 -0
- enigma_api_client/plugins/enigma-api/harness/conftest.py +42 -0
- enigma_api_client/plugins/enigma-api/harness/test_linter.py +863 -0
- enigma_api_client/plugins/enigma-api/harness/test_queries.py +143 -0
- enigma_api_client/plugins/enigma-api/harness/test_skill_rules.py +351 -0
- enigma_api_client/plugins/enigma-api/pyproject.toml +9 -0
- enigma_api_client/plugins/enigma-api/scripts/__pycache__/lint_query.cpython-313.pyc +0 -0
- enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-313.pyc +0 -0
- enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-314.pyc +0 -0
- enigma_api_client/plugins/enigma-api/scripts/generate_report.py +384 -0
- enigma_api_client/plugins/enigma-api/scripts/lint_query.py +712 -0
- enigma_api_client/plugins/enigma-api/scripts/query_validator.py +352 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/SKILL.md +620 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/03_brand_profile.md +163 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/13_brand_dump.md +95 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/derived/07_coalesce_ein_formation.md +146 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/01_coffee_shops_zip.md +287 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/11_ol_industry_discovery.md +116 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/export/08_segment_export.md +96 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/02_officers_lookup.md +366 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/10_kyb_verify.md +176 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/legal_entity/09_legal_entity_profile.md +97 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/locations/05_brand_locations_by_state.md +120 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/person_search/04_find_person_companies.md +160 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/revenue/06_brand_revenue.md +176 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/risk/12_risk_signals.md +118 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/card_transactions.md +91 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/field_corrections.json +106 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/filter_syntax.md +203 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/naics_2022_mapping.json +184 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/pagination_template.py +115 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/schema.graphql +44971 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/test_examples.py +1122 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/validate_query.py +177 -0
- enigma_api_client/plugins/enigma-api/test-cases/graphql-test-cases.md +196 -0
- enigma_api_client/plugins/enigma-api/tests/accuracy_cases.csv +116 -0
- enigma_api_client/plugins/enigma-api/tests/applicability_review.csv +64 -0
- enigma_api_client/plugins/enigma-api/tests/parallel-ground-truth.csv +101 -0
- enigma_api_client/plugins/enigma-api/tests/run_tests.sh +320 -0
- enigma_api_client/plugins/enigma-api/tests/test_cases.yaml +213 -0
- enigma_api_client/plugins/enigma-api/tests/validation_cases.csv +101 -0
- enigma_api_client/py.typed +0 -0
- enigma_api_client/query_router/__init__.py +3 -0
- enigma_api_client/query_router/geo.py +121 -0
- enigma_api_client/query_router/templates.py +475 -0
- enigma_api_client/strategies/__init__.py +16 -0
- enigma_api_client/strategies/agent.py +63 -0
- enigma_api_client/strategies/base.py +15 -0
- enigma_api_client/strategies/generative.py +143 -0
- enigma_api_client/strategies/llm_provider.py +137 -0
- enigma_api_client/strategies/service.py +103 -0
- enigma_api_client/strategies/template.py +189 -0
- enigma_api_client-0.3.1.dist-info/METADATA +257 -0
- enigma_api_client-0.3.1.dist-info/RECORD +63 -0
- enigma_api_client-0.3.1.dist-info/WHEEL +5 -0
- enigma_api_client-0.3.1.dist-info/licenses/LICENSE +201 -0
- enigma_api_client-0.3.1.dist-info/top_level.txt +5 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# ABOUTME: Generates a concise MR-ready validation report for SKILL.md
|
|
2
|
+
# ABOUTME: Runs linter, query checks, and structural rules; outputs markdown summary
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Enigma GraphQL Skill — Validation Report Generator
|
|
6
|
+
|
|
7
|
+
Runs all checks against SKILL.md and prints a concise markdown report
|
|
8
|
+
suitable for pasting into an MR description or CI output.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python scripts/generate_report.py # default: skills/enigma-graphql/SKILL.md
|
|
12
|
+
python scripts/generate_report.py path/to/SKILL.md # custom path
|
|
13
|
+
|
|
14
|
+
Exit code: 0 if all critical checks pass, 1 otherwise.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Add scripts/ to path
|
|
25
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
26
|
+
PLUGIN_ROOT = SCRIPTS_DIR.parent
|
|
27
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
28
|
+
|
|
29
|
+
from lint_query import lint_query, Severity # noqa: E402
|
|
30
|
+
from query_validator import ( # noqa: E402
|
|
31
|
+
validate_query,
|
|
32
|
+
extract_graphql_blocks_with_context,
|
|
33
|
+
is_complete_query,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Check definitions
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
STRUCTURAL_CHECKS: list[dict] = [
|
|
42
|
+
{
|
|
43
|
+
"id": "S-01",
|
|
44
|
+
"name": "No limit:50 in JSON blocks",
|
|
45
|
+
"check": lambda c: not bool(re.search(r'"limit"\s*:\s*50\b', _json_blocks(c))),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "S-02",
|
|
49
|
+
"name": "Discovery limits ≤ 10",
|
|
50
|
+
"check": lambda c: _check_discovery_limits(c),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "S-03",
|
|
54
|
+
"name": "Discovery OL first ≤ 5",
|
|
55
|
+
"check": lambda c: _check_discovery_ol_first(c),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "S-04",
|
|
59
|
+
"name": "No platformBrandId in OL recipes",
|
|
60
|
+
"check": lambda c: "platformBrandId" not in _get_section_blocks(c, r"Operating Location.*Revenue", "all"),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "S-05",
|
|
64
|
+
"name": "platformBrandId in Brand revenue",
|
|
65
|
+
"check": lambda c: "platformBrandId" in _get_section_blocks(c, r"Brand Revenue Query", "json"),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "S-06",
|
|
69
|
+
"name": "Singular count fields in aggregate",
|
|
70
|
+
"check": lambda c: _check_aggregate_singular(c),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "S-07",
|
|
74
|
+
"name": "Plural count fields in search",
|
|
75
|
+
"check": lambda c: _check_search_plural(c),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "S-08",
|
|
79
|
+
"name": "@coalesce paired with @skip",
|
|
80
|
+
"check": lambda c: _check_coalesce_skip(c),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "S-09",
|
|
84
|
+
"name": "street1 AGENT BEHAVIOR warning",
|
|
85
|
+
"check": lambda c: "AGENT BEHAVIOR" in c and "street" in c[c.index("AGENT BEHAVIOR"):c.index("AGENT BEHAVIOR") + 500].lower(),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "S-10",
|
|
89
|
+
"name": "Franchise/chain warning present",
|
|
90
|
+
"check": lambda c: bool(re.search(r"Franchise.*Chain.*Resolution|Chain.*Resolution", c, re.IGNORECASE)),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "S-11",
|
|
94
|
+
"name": "No totalCount in code blocks",
|
|
95
|
+
"check": lambda c: "totalCount" not in _graphql_blocks_text(c),
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Helpers for structural checks
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def _json_blocks(content: str) -> str:
|
|
105
|
+
"""Concatenate all ```json blocks."""
|
|
106
|
+
return "\n".join(m.group(1) for m in re.finditer(r"^```json\s*\n(.*?)\n```", content, re.M | re.S))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _graphql_blocks_text(content: str) -> str:
|
|
110
|
+
"""Concatenate all ```graphql blocks."""
|
|
111
|
+
return "\n".join(m.group(1) for m in re.finditer(r"^```graphql\s*\n(.*?)\n```", content, re.M | re.S))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _get_section(content: str, heading_pattern: str) -> str | None:
|
|
115
|
+
lines = content.split("\n")
|
|
116
|
+
capturing = False
|
|
117
|
+
captured: list[str] = []
|
|
118
|
+
heading_level = None
|
|
119
|
+
for line in lines:
|
|
120
|
+
hm = re.match(r"^(#{1,4})\s+(.*)", line)
|
|
121
|
+
if hm:
|
|
122
|
+
level = len(hm.group(1))
|
|
123
|
+
text = hm.group(2).strip()
|
|
124
|
+
if re.search(heading_pattern, text, re.IGNORECASE):
|
|
125
|
+
capturing = True
|
|
126
|
+
heading_level = level
|
|
127
|
+
captured.append(line)
|
|
128
|
+
continue
|
|
129
|
+
elif capturing and level <= heading_level:
|
|
130
|
+
break
|
|
131
|
+
if capturing:
|
|
132
|
+
captured.append(line)
|
|
133
|
+
return "\n".join(captured) if captured else None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_section_blocks(content: str, heading_pattern: str, block_type: str) -> str:
|
|
137
|
+
sec = _get_section(content, heading_pattern)
|
|
138
|
+
if not sec:
|
|
139
|
+
return ""
|
|
140
|
+
if block_type == "json":
|
|
141
|
+
return _json_blocks(sec)
|
|
142
|
+
elif block_type == "graphql":
|
|
143
|
+
return _graphql_blocks_text(sec)
|
|
144
|
+
else:
|
|
145
|
+
return _json_blocks(sec) + "\n" + _graphql_blocks_text(sec)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _check_discovery_limits(content: str) -> bool:
|
|
149
|
+
sec = _get_section(content, r"Discovery.*Find All|Discovery.*Businesses")
|
|
150
|
+
if not sec:
|
|
151
|
+
return True
|
|
152
|
+
for m in re.finditer(r'"limit"\s*:\s*(\d+)', _json_blocks(sec)):
|
|
153
|
+
if int(m.group(1)) > 10:
|
|
154
|
+
return False
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _check_discovery_ol_first(content: str) -> bool:
|
|
159
|
+
sec = _get_section(content, r"Discovery.*Find All|Discovery.*Businesses")
|
|
160
|
+
if not sec:
|
|
161
|
+
return True
|
|
162
|
+
for m in re.finditer(r"operatingLocations\s*\(\s*first\s*:\s*(\d+)", _graphql_blocks_text(sec)):
|
|
163
|
+
if int(m.group(1)) > 5:
|
|
164
|
+
return False
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _check_aggregate_singular(content: str) -> bool:
|
|
169
|
+
sec = _get_section(content, r"Aggregate Query")
|
|
170
|
+
if not sec:
|
|
171
|
+
return True
|
|
172
|
+
# Stop at Count on Search subsection
|
|
173
|
+
lines = sec.split("\n")
|
|
174
|
+
trimmed = []
|
|
175
|
+
for line in lines:
|
|
176
|
+
if re.match(r"^#{1,4}\s+Count on Search", line, re.IGNORECASE):
|
|
177
|
+
break
|
|
178
|
+
trimmed.append(line)
|
|
179
|
+
gql = _graphql_blocks_text("\n".join(trimmed))
|
|
180
|
+
for m in re.finditer(r'count\s*\(\s*field\s*:\s*"(\w+)"', gql):
|
|
181
|
+
if m.group(1) in ("operatingLocations", "legalEntities", "brands"):
|
|
182
|
+
return False
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _check_search_plural(content: str) -> bool:
|
|
187
|
+
sec = _get_section(content, r"Count on Search")
|
|
188
|
+
if not sec:
|
|
189
|
+
return True
|
|
190
|
+
gql = _graphql_blocks_text(sec)
|
|
191
|
+
for m in re.finditer(r'count\s*\(\s*field\s*:\s*"(\w+)"', gql):
|
|
192
|
+
if m.group(1) in ("operatingLocation", "legalEntity", "brand"):
|
|
193
|
+
return False
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _check_coalesce_skip(content: str) -> bool:
|
|
198
|
+
for m in re.finditer(r"^```graphql\s*\n(.*?)\n```", content, re.M | re.S):
|
|
199
|
+
block = m.group(1)
|
|
200
|
+
if "@coalesce" in block and "@skip(if: true)" not in block and "@skip(if:true)" not in block:
|
|
201
|
+
return False
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# Report generator
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def generate_report(skill_path: Path) -> tuple[str, bool]:
|
|
210
|
+
"""Generate a markdown validation report. Returns (report_text, all_passed)."""
|
|
211
|
+
content = skill_path.read_text(encoding="utf-8")
|
|
212
|
+
blocks = extract_graphql_blocks_with_context(content)
|
|
213
|
+
|
|
214
|
+
lines: list[str] = []
|
|
215
|
+
all_passed = True
|
|
216
|
+
|
|
217
|
+
# Header
|
|
218
|
+
lines.append("## SKILL.md Validation Report")
|
|
219
|
+
lines.append("")
|
|
220
|
+
lines.append(f"**File**: `{skill_path.name}` | **Date**: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
|
|
221
|
+
lines.append("")
|
|
222
|
+
|
|
223
|
+
# ── Section 1: Structural checks ──────────────────────────────
|
|
224
|
+
lines.append("### Structural Checks")
|
|
225
|
+
lines.append("")
|
|
226
|
+
lines.append("| # | Check | Result |")
|
|
227
|
+
lines.append("|---|-------|--------|")
|
|
228
|
+
|
|
229
|
+
structural_pass = 0
|
|
230
|
+
structural_fail = 0
|
|
231
|
+
for check in STRUCTURAL_CHECKS:
|
|
232
|
+
try:
|
|
233
|
+
passed = check["check"](content)
|
|
234
|
+
except Exception:
|
|
235
|
+
passed = False
|
|
236
|
+
icon = "✅" if passed else "❌"
|
|
237
|
+
if not passed:
|
|
238
|
+
structural_fail += 1
|
|
239
|
+
all_passed = False
|
|
240
|
+
else:
|
|
241
|
+
structural_pass += 1
|
|
242
|
+
lines.append(f"| {check['id']} | {check['name']} | {icon} |")
|
|
243
|
+
|
|
244
|
+
lines.append("")
|
|
245
|
+
lines.append(f"> {structural_pass} passed, {structural_fail} failed")
|
|
246
|
+
lines.append("")
|
|
247
|
+
|
|
248
|
+
# ── Section 2: Query validation ───────────────────────────────
|
|
249
|
+
complete = [b for b in blocks if is_complete_query(b["query"])]
|
|
250
|
+
snippets = [b for b in blocks if not is_complete_query(b["query"])]
|
|
251
|
+
|
|
252
|
+
syntax_pass = 0
|
|
253
|
+
syntax_fail = 0
|
|
254
|
+
lint_pass = 0
|
|
255
|
+
lint_fail = 0
|
|
256
|
+
lint_failures: list[str] = []
|
|
257
|
+
|
|
258
|
+
for block in complete:
|
|
259
|
+
q = block["query"]
|
|
260
|
+
section = block.get("section", "?")
|
|
261
|
+
line = block.get("line_number", 0)
|
|
262
|
+
label = f"L{line} {section}"[:60]
|
|
263
|
+
|
|
264
|
+
# Syntax
|
|
265
|
+
vr = validate_query(q)
|
|
266
|
+
if vr.valid:
|
|
267
|
+
syntax_pass += 1
|
|
268
|
+
else:
|
|
269
|
+
syntax_fail += 1
|
|
270
|
+
all_passed = False
|
|
271
|
+
|
|
272
|
+
# Lint (errors only)
|
|
273
|
+
lr = lint_query(q)
|
|
274
|
+
errors = [f for f in lr.findings if f.severity == Severity.ERROR]
|
|
275
|
+
if errors:
|
|
276
|
+
lint_fail += 1
|
|
277
|
+
all_passed = False
|
|
278
|
+
rules = ", ".join(sorted({f.rule_id for f in errors}))
|
|
279
|
+
lint_failures.append(f" - **{label}**: {rules}")
|
|
280
|
+
else:
|
|
281
|
+
lint_pass += 1
|
|
282
|
+
|
|
283
|
+
lines.append("### Query Validation")
|
|
284
|
+
lines.append("")
|
|
285
|
+
lines.append(f"**{len(complete)}** complete queries found, **{len(snippets)}** snippets (skipped)")
|
|
286
|
+
lines.append("")
|
|
287
|
+
lines.append("| Check | Passed | Failed |")
|
|
288
|
+
lines.append("|-------|--------|--------|")
|
|
289
|
+
lines.append(f"| Syntax (balanced braces, structure) | {syntax_pass} | {syntax_fail} |")
|
|
290
|
+
lines.append(f"| Lint (ERROR-severity rules) | {lint_pass} | {lint_fail} |")
|
|
291
|
+
lines.append("")
|
|
292
|
+
|
|
293
|
+
if lint_failures:
|
|
294
|
+
lines.append("<details><summary>Lint failures</summary>")
|
|
295
|
+
lines.append("")
|
|
296
|
+
lines.extend(lint_failures)
|
|
297
|
+
lines.append("")
|
|
298
|
+
lines.append("</details>")
|
|
299
|
+
lines.append("")
|
|
300
|
+
|
|
301
|
+
# ── Section 3: Per-rule results ─────────────────────────────
|
|
302
|
+
rule_descriptions = {
|
|
303
|
+
"ENIGMA-001": ("ERROR", "LegalEntity.persons usage (usually empty)"),
|
|
304
|
+
"ENIGMA-002": ("ERROR", "Direct roles on LegalEntity"),
|
|
305
|
+
"ENIGMA-003": ("ERROR", "Missing pagination (first: N)"),
|
|
306
|
+
"ENIGMA-004": ("ERROR", "Missing entityType on searchInput"),
|
|
307
|
+
"ENIGMA-005": ("ERROR", "prompt with non-BRAND entity"),
|
|
308
|
+
"ENIGMA-006": ("ERROR", "Unfiltered cardTransactions"),
|
|
309
|
+
"ENIGMA-007": ("ERROR", "Deep traversal (>5 levels)"),
|
|
310
|
+
"ENIGMA-008": ("ERROR", "roles on Brand (invalid field)"),
|
|
311
|
+
"ENIGMA-009": ("ERROR", "Multi-field filter without AND/OR"),
|
|
312
|
+
"ENIGMA-010": ("ERROR", "Missing search limit"),
|
|
313
|
+
"ENIGMA-011": ("WARNING", "Unreliable HAS operator"),
|
|
314
|
+
"ENIGMA-012": ("WARNING", "Conditions on unsupported fields"),
|
|
315
|
+
"ENIGMA-013": ("WARNING", "Address-only search"),
|
|
316
|
+
"ENIGMA-014": ("WARNING", "rawQuantity instead of projectedQuantity"),
|
|
317
|
+
"ENIGMA-015": ("WARNING", "postalCode in return fields (use zip)"),
|
|
318
|
+
"ENIGMA-016": ("INFO", "Address ranking on PERSON search"),
|
|
319
|
+
"ENIGMA-017": ("INFO", "LegalEntity → Brand ~30% error rate"),
|
|
320
|
+
"ENIGMA-018": ("INFO", "Missing operation name"),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Count how many queries triggered each rule
|
|
324
|
+
rule_trigger_count: dict[str, int] = {rid: 0 for rid in rule_descriptions}
|
|
325
|
+
for block in complete:
|
|
326
|
+
lr = lint_query(block["query"])
|
|
327
|
+
for f in lr.findings:
|
|
328
|
+
if f.rule_id in rule_trigger_count:
|
|
329
|
+
rule_trigger_count[f.rule_id] += 1
|
|
330
|
+
|
|
331
|
+
lines.append("### Lint Rules")
|
|
332
|
+
lines.append("")
|
|
333
|
+
lines.append(f"| Rule | Severity | Description | Result |")
|
|
334
|
+
lines.append("|------|----------|-------------|--------|")
|
|
335
|
+
for rule_id, (sev, desc) in rule_descriptions.items():
|
|
336
|
+
count = rule_trigger_count[rule_id]
|
|
337
|
+
if count == 0:
|
|
338
|
+
icon = "✅ Pass"
|
|
339
|
+
elif sev == "INFO":
|
|
340
|
+
icon = f"ℹ️ {count} note(s)"
|
|
341
|
+
elif sev == "WARNING":
|
|
342
|
+
icon = f"⚠️ {count} warning(s)"
|
|
343
|
+
else:
|
|
344
|
+
icon = f"❌ {count} error(s)"
|
|
345
|
+
lines.append(f"| {rule_id} | {sev} | {desc} | {icon} |")
|
|
346
|
+
|
|
347
|
+
lines.append("")
|
|
348
|
+
|
|
349
|
+
# ── Footer ────────────────────────────────────────────────────
|
|
350
|
+
total_checks = structural_pass + structural_fail + syntax_pass + syntax_fail + lint_pass + lint_fail
|
|
351
|
+
total_passed = structural_pass + syntax_pass + lint_pass
|
|
352
|
+
total_failed = structural_fail + syntax_fail + lint_fail
|
|
353
|
+
|
|
354
|
+
if all_passed:
|
|
355
|
+
lines.append(f"**Result: ✅ ALL CHECKS PASSED** ({total_passed}/{total_checks})")
|
|
356
|
+
else:
|
|
357
|
+
lines.append(f"**Result: ❌ {total_failed} CHECK(S) FAILED** ({total_passed}/{total_checks} passed)")
|
|
358
|
+
|
|
359
|
+
lines.append("")
|
|
360
|
+
|
|
361
|
+
return "\n".join(lines), all_passed
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
# CLI
|
|
366
|
+
# ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
def main():
|
|
369
|
+
if len(sys.argv) > 1:
|
|
370
|
+
skill_path = Path(sys.argv[1])
|
|
371
|
+
else:
|
|
372
|
+
skill_path = PLUGIN_ROOT / "skills" / "enigma-graphql" / "SKILL.md"
|
|
373
|
+
|
|
374
|
+
if not skill_path.exists():
|
|
375
|
+
print(f"ERROR: {skill_path} not found", file=sys.stderr)
|
|
376
|
+
sys.exit(1)
|
|
377
|
+
|
|
378
|
+
report, passed = generate_report(skill_path)
|
|
379
|
+
print(report)
|
|
380
|
+
sys.exit(0 if passed else 1)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
if __name__ == "__main__":
|
|
384
|
+
main()
|