cernitur-lint 1.0.0__tar.gz

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.
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: cernitur-lint
3
+ Version: 1.0.0
4
+ Summary: Deterministic Answer Engine Optimization (AEO) and JSON-LD schema linter for Google AI Overviews and ChatGPT.
5
+ Author-email: Cernitur Technologies LLC <support@cernitur.com>
6
+ Project-URL: Homepage, https://cernitur.com
7
+ Project-URL: Documentation, https://cernitur.com/guides/json-ld-entity-graph
8
+ Project-URL: Repository, https://github.com/cernitur/aeo-lint
9
+ Project-URL: Issues, https://github.com/cernitur/aeo-lint/issues
10
+ Keywords: aeo,geo,seo,schema,llms-txt,cernitur
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: urllib3>=1.26.0
18
+
19
+ # cernitur-lint
20
+
21
+ > **Deterministic Answer Engine Optimization (AEO) and Schema.org Linter.**
22
+ > Maintained by [Cernitur Technologies LLC](https://cernitur.com).
23
+
24
+ Inspects web domains for compliance with **Google AI Overviews, OpenAI Search (ChatGPT), Claude, and Perplexity** across robots.txt AI crawlers, `/llms.txt` specifications, and 5-dimension Schema.org `@graph` entity architectures.
25
+
26
+ ---
27
+
28
+ ## ⚡ Installation & Usage
29
+
30
+ ```bash
31
+ pip install cernitur-lint
32
+ cernitur-lint yourdomain.com
33
+ ```
34
+
35
+ Or output raw JSON:
36
+
37
+ ```bash
38
+ cernitur-lint yourdomain.com --json
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 🌐 Official Links
44
+
45
+ - **Hosted Diagnostic Scanner:** [https://cernitur.com/scanner](https://cernitur.com/scanner)
46
+ - **One-Time AEO Architecture Suite:** [https://cernitur.com/pricing](https://cernitur.com/pricing)
47
+ - **AEO vs. Retainers Comparison:** [https://cernitur.com/compare/aeo-platforms](https://cernitur.com/compare/aeo-platforms)
48
+
49
+ License: Apache-2.0 &copy; 2026 Cernitur Technologies LLC.
@@ -0,0 +1,31 @@
1
+ # cernitur-lint
2
+
3
+ > **Deterministic Answer Engine Optimization (AEO) and Schema.org Linter.**
4
+ > Maintained by [Cernitur Technologies LLC](https://cernitur.com).
5
+
6
+ Inspects web domains for compliance with **Google AI Overviews, OpenAI Search (ChatGPT), Claude, and Perplexity** across robots.txt AI crawlers, `/llms.txt` specifications, and 5-dimension Schema.org `@graph` entity architectures.
7
+
8
+ ---
9
+
10
+ ## ⚡ Installation & Usage
11
+
12
+ ```bash
13
+ pip install cernitur-lint
14
+ cernitur-lint yourdomain.com
15
+ ```
16
+
17
+ Or output raw JSON:
18
+
19
+ ```bash
20
+ cernitur-lint yourdomain.com --json
21
+ ```
22
+
23
+ ---
24
+
25
+ ## 🌐 Official Links
26
+
27
+ - **Hosted Diagnostic Scanner:** [https://cernitur.com/scanner](https://cernitur.com/scanner)
28
+ - **One-Time AEO Architecture Suite:** [https://cernitur.com/pricing](https://cernitur.com/pricing)
29
+ - **AEO vs. Retainers Comparison:** [https://cernitur.com/compare/aeo-platforms](https://cernitur.com/compare/aeo-platforms)
30
+
31
+ License: Apache-2.0 &copy; 2026 Cernitur Technologies LLC.
@@ -0,0 +1,6 @@
1
+ """
2
+ cernitur-lint: Deterministic AEO & Schema Evaluator CLI.
3
+ Copyright (c) 2026 Cernitur Technologies LLC.
4
+ """
5
+
6
+ __version__ = "1.0.0"
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ cernitur-lint: AEO & Schema Evaluation CLI
4
+ Copyright (c) 2026 Cernitur Technologies LLC
5
+ https://cernitur.com
6
+ """
7
+
8
+ import sys
9
+ import json
10
+ import re
11
+ import urllib.request
12
+ import urllib.error
13
+ import urllib.parse
14
+
15
+ # Ensure UTF-8 output on Windows consoles
16
+ if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
17
+ try:
18
+ sys.stdout.reconfigure(encoding="utf-8")
19
+ except Exception:
20
+ pass
21
+
22
+ def clean_domain(target: str) -> str:
23
+ target = target.strip()
24
+ if not target.startswith("http://") and not target.startswith("https://"):
25
+ target = "https://" + target
26
+ parsed = urllib.parse.urlparse(target)
27
+ return (parsed.hostname or target).lower()
28
+
29
+ def fetch_url(url: str, timeout: int = 8):
30
+ req = urllib.request.Request(
31
+ url,
32
+ headers={"User-Agent": "Cernitur-AEOLint-Py/1.0 (+https://cernitur.com/scanner)"}
33
+ )
34
+ try:
35
+ with urllib.request.urlopen(req, timeout=timeout) as response:
36
+ return response.getcode(), response.headers.get("Content-Type", ""), response.read().decode("utf-8", errors="replace")
37
+ except urllib.error.HTTPError as e:
38
+ return e.code, "", ""
39
+ except Exception:
40
+ return 0, "", ""
41
+
42
+ def evaluate(domain: str):
43
+ # 1. Robots
44
+ robots_code, _, robots_txt = fetch_url(f"https://{domain}/robots.txt")
45
+ robots_score = 0
46
+ robots_checks = []
47
+
48
+ if robots_code == 200:
49
+ robots_score += 5
50
+ robots_checks.append({"name": "robots.txt accessible", "pass": True, "note": f"HTTP 200 ({len(robots_txt)} bytes)"})
51
+ crawlers = ["OAI-SearchBot", "Claude-SearchBot", "PerplexityBot", "Googlebot", "Google-Extended"]
52
+ found = sum(1 for b in crawlers if re.search(rf"User-agent:\s*{b}", robots_txt, re.IGNORECASE))
53
+ if found >= 3:
54
+ robots_score += 10
55
+ robots_checks.append({"name": "AI search crawlers declared", "pass": True, "note": f"{found}/5 verified bots declared"})
56
+ elif found > 0:
57
+ robots_score += 5
58
+ robots_checks.append({"name": "AI search crawlers declared", "pass": True, "note": f"{found}/5 verified bots declared"})
59
+ else:
60
+ robots_checks.append({"name": "AI search crawlers declared", "pass": False, "note": "No explicit AI search crawlers"})
61
+
62
+ if re.search(r"Sitemap:\s*https?://", robots_txt, re.IGNORECASE):
63
+ robots_score += 5
64
+ robots_checks.append({"name": "Sitemap in robots.txt", "pass": True, "note": "Valid Sitemap URL"})
65
+ else:
66
+ robots_checks.append({"name": "Sitemap in robots.txt", "pass": False, "note": "Missing Sitemap directive"})
67
+
68
+ if re.search(r"User-agent:\s*\*\s*\n\s*Disallow:\s*/\s*$", robots_txt, re.IGNORECASE | re.MULTILINE):
69
+ robots_checks.append({"name": "Disallow boundaries", "pass": False, "note": "Blanket Disallow: / blocks AI"})
70
+ else:
71
+ robots_score += 5
72
+ robots_checks.append({"name": "Disallow boundaries", "pass": True, "note": "No blanket Disallow: / blocking bots"})
73
+ else:
74
+ robots_checks.append({"name": "robots.txt accessible", "pass": False, "note": f"HTTP {robots_code}"})
75
+
76
+ # 2. llms.txt
77
+ llms_code, llms_mime, llms_txt = fetch_url(f"https://{domain}/llms.txt")
78
+ llms_score = 0
79
+ llms_checks = []
80
+
81
+ if llms_code == 200:
82
+ llms_score += 5
83
+ llms_checks.append({"name": "llms.txt accessible", "pass": True, "note": f"HTTP 200 ({len(llms_txt)} bytes)"})
84
+ if "text/plain" in llms_mime or "text/markdown" in llms_mime:
85
+ llms_score += 5
86
+ llms_checks.append({"name": "llms.txt MIME type", "pass": True, "note": llms_mime.split(";")[0]})
87
+ else:
88
+ llms_checks.append({"name": "llms.txt MIME type", "pass": False, "note": f"Non-standard MIME ({llms_mime})"})
89
+
90
+ if re.search(r"^#\s+.+", llms_txt, re.MULTILINE):
91
+ llms_score += 5
92
+ llms_checks.append({"name": "H1 Title Heading", "pass": True, "note": "Valid top-level heading"})
93
+ else:
94
+ llms_checks.append({"name": "H1 Title Heading", "pass": False, "note": "Missing # Title"})
95
+
96
+ if re.search(r"^>\s+.+", llms_txt, re.MULTILINE):
97
+ llms_score += 5
98
+ llms_checks.append({"name": "Entity summary blockquote", "pass": True, "note": "Found canonical blockquote"})
99
+ else:
100
+ llms_checks.append({"name": "Entity summary blockquote", "pass": False, "note": "Missing > summary"})
101
+
102
+ links = re.findall(r"\[([^\]]+)\]\(([^)]+)\)", llms_txt)
103
+ if len(links) >= 3:
104
+ llms_score += 5
105
+ llms_checks.append({"name": "Markdown navigation links", "pass": True, "note": f"{len(links)} canonical links"})
106
+ else:
107
+ llms_checks.append({"name": "Markdown navigation links", "pass": False, "note": f"{len(links)} links found"})
108
+ else:
109
+ llms_checks.append({"name": "llms.txt accessible", "pass": False, "note": f"HTTP {llms_code}"})
110
+
111
+ # 3. HTML & Schema
112
+ html_code, _, html_text = fetch_url(f"https://{domain}/")
113
+ html_score = 0
114
+ html_checks = []
115
+
116
+ if html_code == 200:
117
+ if len(html_text) > 2500 and '<div id="__next"></div></body>' not in html_text:
118
+ html_score += 10
119
+ html_checks.append({"name": "SSR HTML density", "pass": True, "note": f"{round(len(html_text)/1024)} KB markup"})
120
+ else:
121
+ html_checks.append({"name": "SSR HTML density", "pass": False, "note": "Low initial HTML payload"})
122
+
123
+ if "<title" in html_text and ('rel="canonical"' in html_text or "rel='canonical'" in html_text):
124
+ html_score += 5
125
+ html_checks.append({"name": "SEO metadata", "pass": True, "note": "Title and canonical present"})
126
+ else:
127
+ html_checks.append({"name": "SEO metadata", "pass": False, "note": "Missing title or canonical"})
128
+
129
+ # Extract JSON-LD
130
+ blocks = re.findall(r'<script\b[^>]*type=["\']application/ld\+json["\'][^>]*>([\s\S]*?)</script>', html_text, re.IGNORECASE)
131
+ nodes = []
132
+ for b in blocks:
133
+ try:
134
+ parsed = json.loads(b)
135
+ if isinstance(parsed, dict) and "@graph" in parsed and isinstance(parsed["@graph"], list):
136
+ nodes.extend(parsed["@graph"])
137
+ elif isinstance(parsed, list):
138
+ nodes.extend(parsed)
139
+ elif isinstance(parsed, dict):
140
+ nodes.append(parsed)
141
+ except Exception:
142
+ pass
143
+
144
+ if nodes:
145
+ html_score += 5
146
+ html_checks.append({"name": "JSON-LD structured data", "pass": True, "note": f"{len(nodes)} entity node(s)"})
147
+
148
+ has_org = any(n.get("@type") in ["Organization", "LocalBusiness", "Corporation"] for n in nodes)
149
+ has_same_as = any(isinstance(n.get("sameAs"), list) and len(n.get("sameAs")) > 0 for n in nodes)
150
+ if has_org and has_same_as:
151
+ html_score += 8
152
+ html_checks.append({"name": "Schema: Identity & sameAs", "pass": True, "note": "Organization with sameAs"})
153
+ elif has_org:
154
+ html_score += 4
155
+ html_checks.append({"name": "Schema: Identity & sameAs", "pass": True, "note": "Organization found (sameAs missing)"})
156
+ else:
157
+ html_checks.append({"name": "Schema: Identity & sameAs", "pass": False, "note": "Missing Organization node"})
158
+
159
+ if any(n.get("knowsAbout") or n.get("disambiguatingDescription") for n in nodes):
160
+ html_score += 8
161
+ html_checks.append({"name": "Schema: Ontology Disambiguation", "pass": True, "note": "Declared knowsAbout/disambiguation"})
162
+ else:
163
+ html_checks.append({"name": "Schema: Ontology Disambiguation", "pass": False, "note": "Missing topic authority"})
164
+
165
+ if any(n.get("address") or n.get("areaServed") or n.get("geo") for n in nodes):
166
+ html_score += 7
167
+ html_checks.append({"name": "Schema: Geographic Precision", "pass": True, "note": "PostalAddress or areaServed"})
168
+ else:
169
+ html_checks.append({"name": "Schema: Geographic Precision", "pass": False, "note": "No geographic tags"})
170
+
171
+ if any(n.get("contactPoint") or n.get("foundingDate") for n in nodes):
172
+ html_score += 7
173
+ html_checks.append({"name": "Schema: E-E-A-T Anchoring", "pass": True, "note": "ContactPoint or founding date"})
174
+ else:
175
+ html_checks.append({"name": "Schema: E-E-A-T Anchoring", "pass": False, "note": "No ContactPoint found"})
176
+ else:
177
+ html_checks.append({"name": "JSON-LD structured data", "pass": False, "note": "No valid JSON-LD parsed"})
178
+ else:
179
+ html_checks.append({"name": "Homepage SSR retrieval", "pass": False, "note": f"HTTP {html_code}"})
180
+
181
+ total = robots_score + llms_score + html_score
182
+ return {
183
+ "domain": domain,
184
+ "score": total,
185
+ "max": 100,
186
+ "grade": "A" if total >= 85 else "B" if total >= 70 else "C" if total >= 50 else "D",
187
+ "pillars": {
188
+ "robots": {"score": robots_score, "max": 25, "checks": robots_checks},
189
+ "llms": {"score": llms_score, "max": 25, "checks": llms_checks},
190
+ "schema": {"score": html_score, "max": 50, "checks": html_checks},
191
+ },
192
+ "reportUrl": f"https://cernitur.com/report/{urllib.parse.quote(domain)}"
193
+ }
194
+
195
+ def main():
196
+ if len(sys.argv) < 2 or sys.argv[1] in ["-h", "--help"]:
197
+ print("Usage: cernitur-lint <domain-or-url> [--json]")
198
+ print("Answer Engine Optimization (AEO) diagnostic CLI by Cernitur Technologies LLC")
199
+ sys.exit(0)
200
+
201
+ is_json = "--json" in sys.argv
202
+ target = [a for a in sys.argv[1:] if not a.startswith("-")][0]
203
+ domain = clean_domain(target)
204
+
205
+ result = evaluate(domain)
206
+
207
+ if is_json:
208
+ print(json.dumps(result, indent=2))
209
+ return
210
+
211
+ print(f"\nCernitur AEO Linter (Python) - Target: https://{domain}")
212
+ print("=" * 65)
213
+ print(f"1. AI Crawlers & Robots.txt ({result['pillars']['robots']['score']}/25 pts)")
214
+ for c in result['pillars']['robots']['checks']:
215
+ sym = "[PASS]" if c['pass'] else "[FAIL]"
216
+ print(f" {sym:<6} {c['name']:<35} {c['note']}")
217
+
218
+ print(f"\n2. Machine-Readable Endpoint (/llms.txt) ({result['pillars']['llms']['score']}/25 pts)")
219
+ for c in result['pillars']['llms']['checks']:
220
+ sym = "[PASS]" if c['pass'] else "[FAIL]"
221
+ print(f" {sym:<6} {c['name']:<35} {c['note']}")
222
+
223
+ print(f"\n3. SSR Payload Density & Schema Graph ({result['pillars']['schema']['score']}/50 pts)")
224
+ for c in result['pillars']['schema']['checks']:
225
+ sym = "[PASS]" if c['pass'] else "[FAIL]"
226
+ print(f" {sym:<6} {c['name']:<35} {c['note']}")
227
+
228
+ print("=" * 65)
229
+ print(f"TOTAL AEO READINESS SCORE: {result['score']}/100 (Grade {result['grade']})")
230
+ print(f"Full Report: {result['reportUrl']}\n")
231
+
232
+ if __name__ == "__main__":
233
+ main()
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: cernitur-lint
3
+ Version: 1.0.0
4
+ Summary: Deterministic Answer Engine Optimization (AEO) and JSON-LD schema linter for Google AI Overviews and ChatGPT.
5
+ Author-email: Cernitur Technologies LLC <support@cernitur.com>
6
+ Project-URL: Homepage, https://cernitur.com
7
+ Project-URL: Documentation, https://cernitur.com/guides/json-ld-entity-graph
8
+ Project-URL: Repository, https://github.com/cernitur/aeo-lint
9
+ Project-URL: Issues, https://github.com/cernitur/aeo-lint/issues
10
+ Keywords: aeo,geo,seo,schema,llms-txt,cernitur
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: urllib3>=1.26.0
18
+
19
+ # cernitur-lint
20
+
21
+ > **Deterministic Answer Engine Optimization (AEO) and Schema.org Linter.**
22
+ > Maintained by [Cernitur Technologies LLC](https://cernitur.com).
23
+
24
+ Inspects web domains for compliance with **Google AI Overviews, OpenAI Search (ChatGPT), Claude, and Perplexity** across robots.txt AI crawlers, `/llms.txt` specifications, and 5-dimension Schema.org `@graph` entity architectures.
25
+
26
+ ---
27
+
28
+ ## ⚡ Installation & Usage
29
+
30
+ ```bash
31
+ pip install cernitur-lint
32
+ cernitur-lint yourdomain.com
33
+ ```
34
+
35
+ Or output raw JSON:
36
+
37
+ ```bash
38
+ cernitur-lint yourdomain.com --json
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 🌐 Official Links
44
+
45
+ - **Hosted Diagnostic Scanner:** [https://cernitur.com/scanner](https://cernitur.com/scanner)
46
+ - **One-Time AEO Architecture Suite:** [https://cernitur.com/pricing](https://cernitur.com/pricing)
47
+ - **AEO vs. Retainers Comparison:** [https://cernitur.com/compare/aeo-platforms](https://cernitur.com/compare/aeo-platforms)
48
+
49
+ License: Apache-2.0 &copy; 2026 Cernitur Technologies LLC.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ cernitur_lint/__init__.py
4
+ cernitur_lint/cli.py
5
+ cernitur_lint.egg-info/PKG-INFO
6
+ cernitur_lint.egg-info/SOURCES.txt
7
+ cernitur_lint.egg-info/dependency_links.txt
8
+ cernitur_lint.egg-info/entry_points.txt
9
+ cernitur_lint.egg-info/requires.txt
10
+ cernitur_lint.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cernitur-lint = cernitur_lint.cli:main
@@ -0,0 +1 @@
1
+ urllib3>=1.26.0
@@ -0,0 +1 @@
1
+ cernitur_lint
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cernitur-lint"
7
+ version = "1.0.0"
8
+ authors = [
9
+ { name="Cernitur Technologies LLC", email="support@cernitur.com" },
10
+ ]
11
+ description = "Deterministic Answer Engine Optimization (AEO) and JSON-LD schema linter for Google AI Overviews and ChatGPT."
12
+ readme = "README.md"
13
+ requires-python = ">=3.9"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
19
+ ]
20
+ keywords = ["aeo", "geo", "seo", "schema", "llms-txt", "cernitur"]
21
+ dependencies = [
22
+ "urllib3>=1.26.0",
23
+ ]
24
+
25
+ [project.scripts]
26
+ cernitur-lint = "cernitur_lint.cli:main"
27
+
28
+ [project.urls]
29
+ Homepage = "https://cernitur.com"
30
+ Documentation = "https://cernitur.com/guides/json-ld-entity-graph"
31
+ Repository = "https://github.com/cernitur/aeo-lint"
32
+ Issues = "https://github.com/cernitur/aeo-lint/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+