Skip to content

Commit f6a00d4

Browse files
authored
feat: add curated llms.txt with validation CI (#1751)
* Add curated llms.txt and CI validation for agent discoverability * Remove non-production strategy memo from public PR * chore: keep llms PR scope focused on production artifacts * fix: address Qodo review findings for llms-check * fix: resolve bot review findings for llms PR * fix: harden llms validator path and URL checks * docs(llms): add H1 summary blockquote * chore(ci): enforce canonical llms URL paths * docs(llms): rebalance curation and allow OpenRPC canonical link
1 parent 6f2c279 commit f6a00d4

3 files changed

Lines changed: 310 additions & 0 deletions

File tree

.github/workflows/llms-check.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: llms-check
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- llms.txt
7+
- scripts/validate_llms_txt.py
8+
- .github/workflows/llms-check.yml
9+
- index.mdx
10+
- build/**
11+
- learn/**
12+
- secure/**
13+
push:
14+
branches:
15+
- main
16+
paths:
17+
- llms.txt
18+
- scripts/validate_llms_txt.py
19+
- .github/workflows/llms-check.yml
20+
- index.mdx
21+
- build/**
22+
- learn/**
23+
- secure/**
24+
25+
concurrency:
26+
group: llms-check-${{ github.workflow }}-${{ github.ref }}
27+
cancel-in-progress: true
28+
29+
permissions:
30+
contents: read
31+
32+
jobs:
33+
validate:
34+
runs-on: ubuntu-latest
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
38+
39+
- name: Setup Python
40+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
41+
with:
42+
python-version: "3.11"
43+
44+
- name: Validate llms.txt
45+
run: python scripts/validate_llms_txt.py

llms.txt

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Starknet Documentation
2+
3+
> Official Starknet documentation for Cairo developers, protocol engineers, node operators, and ecosystem integrators.
4+
5+
Curated entry points for LLM-powered search, coding assistants, and agent runtimes.
6+
Use this file for high-signal discovery. Use `llms-full.txt` for exhaustive context.
7+
8+
## Getting Started
9+
10+
- [Starknet Docs Home](https://docs.starknet.io/index.md): Primary docs landing page for building, learning, and securing Starknet.
11+
- [Build Quickstart Overview](https://docs.starknet.io/build/quickstart/overview.md): End-to-end first contract path from setup to deployment.
12+
- [Environment Setup](https://docs.starknet.io/build/quickstart/environment-setup.md): Toolchain and local environment prerequisites.
13+
- [HelloStarknet Contract Walkthrough](https://docs.starknet.io/build/quickstart/hellostarknet.md): Step-by-step explanation of a minimal Starknet contract.
14+
- [Local Devnet Deployment](https://docs.starknet.io/build/quickstart/devnet.md): Run and test deployment flow locally.
15+
- [Sepolia Deployment](https://docs.starknet.io/build/quickstart/sepolia.md): Deploy and verify on Starknet Sepolia.
16+
17+
## Build and Integrate
18+
19+
- [Starknet by Example](https://docs.starknet.io/build/starknet-by-example/index.md): Cairo and Starknet examples from basics to advanced patterns.
20+
- [Account Abstraction Example](https://docs.starknet.io/build/starknet-by-example/advanced/account-abstraction.md): Practical account abstraction usage patterns.
21+
- [ECDSA Verification Example](https://docs.starknet.io/build/starknet-by-example/advanced/signature-verification.md): Onchain signature verification patterns.
22+
- [Corelib Introduction](https://docs.starknet.io/build/corelib/intro.md): Navigating Starknet/Cairo core library documentation.
23+
- [Starkzap Overview](https://docs.starknet.io/build/starkzap/overview.md): TypeScript SDK for wallet, token, and DeFi integrations.
24+
- [Starkzap Quick Start](https://docs.starknet.io/build/starkzap/quick-start.md): Fastest path to working integration.
25+
- [Starkzap API Reference](https://docs.starknet.io/build/starkzap/api-reference.md): Complete API surface and signatures.
26+
- [Starkzap Transactions](https://docs.starknet.io/build/starkzap/transactions.md): Send, batch, and simulate transactions.
27+
- [Starkzap Wallet Connections](https://docs.starknet.io/build/starkzap/connecting-wallets.md): Supported signer and wallet integration strategies.
28+
- [Using LLMs with Starkzap](https://docs.starknet.io/build/starkzap/using-llms.md): Recommended MCP/assistant workflow for Starknet builds.
29+
30+
## Protocol Fundamentals
31+
32+
- [Learn Starknet](https://docs.starknet.io/learn/intro.md): Concept map for architecture, protocol, and ecosystem.
33+
- [Protocol Introduction](https://docs.starknet.io/learn/protocol/intro.md): Core protocol overview.
34+
- [Accounts](https://docs.starknet.io/learn/protocol/accounts.md): Native account abstraction model.
35+
- [Transactions](https://docs.starknet.io/learn/protocol/transactions.md): Transaction types and lifecycle.
36+
- [Starknet JSON-RPC OpenRPC Spec](https://github.com/starkware-libs/starknet-specs/blob/master/api/starknet_api_openrpc.json): Canonical JSON-RPC schema and method definitions for client and node integrations.
37+
- [Fees](https://docs.starknet.io/learn/protocol/fees.md): Fee mechanics and cost model.
38+
- [L1-L2 Messaging](https://docs.starknet.io/learn/protocol/messaging.md): Cross-layer messaging semantics.
39+
- [Cryptography](https://docs.starknet.io/learn/protocol/cryptography.md): STARK and cryptographic primitives used by Starknet.
40+
- [Data Availability](https://docs.starknet.io/learn/protocol/data-availability.md): Data publishing and availability model.
41+
- [Blocks](https://docs.starknet.io/learn/protocol/blocks.md): Block production and structure.
42+
- [State](https://docs.starknet.io/learn/protocol/state.md): State model and transitions.
43+
- [Staking](https://docs.starknet.io/learn/protocol/staking.md): Staking mechanics and validator economics.
44+
45+
## Security and Operations
46+
47+
- [Secure Starknet Overview](https://docs.starknet.io/secure/quickstart/overview.md): Operator and validator security starting point.
48+
- [Run a Full Node](https://docs.starknet.io/secure/quickstart/running-a-node.md): Operational setup for node operators.
49+
- [Attest to Blocks](https://docs.starknet.io/secure/quickstart/attesting-to-blocks.md): Block attestation workflow.
50+
- [Become a Validator](https://docs.starknet.io/secure/quickstart/becoming-a-validator.md): Validator onboarding path.
51+
- [Secure Starknet Next Steps](https://docs.starknet.io/secure/quickstart/next-steps.md): Post-setup operational guidance.
52+
53+
## Reference and Cheat Sheets
54+
55+
- [Transactions Reference](https://docs.starknet.io/learn/cheatsheets/transactions-reference.md): Transaction field and hash reference.
56+
- [Messaging Reference](https://docs.starknet.io/learn/cheatsheets/messaging-reference.md): L1-L2 messaging function/event reference.
57+
- [Chain Information](https://docs.starknet.io/learn/cheatsheets/chain-info.md): Canonical chain IDs and environment data.
58+
- [Developer Integrations](https://docs.starknet.io/learn/cheatsheets/integrations.md): Integration matrix for ecosystem tooling.
59+
- [Compatibility Tables](https://docs.starknet.io/learn/cheatsheets/compatibility.md): Version compatibility overview.
60+
- [Version Notes](https://docs.starknet.io/learn/cheatsheets/version-notes.md): Protocol and tooling changes across Starknet releases.
61+
- [Developer Tools](https://docs.starknet.io/learn/cheatsheets/tools.md): Tooling index across the stack.
62+
- [S-two Book Introduction](https://docs.starknet.io/learn/S-two-book/introduction.md): STWO educational track entry.
63+
- [S-two Benchmarks Report](https://docs.starknet.io/learn/S-two-book/benchmarks/index.md): Benchmark data and performance context.
64+
65+
## Discovery Artifacts
66+
67+
- [Full LLM Context](https://docs.starknet.io/llms-full.txt): Exhaustive machine-readable docs context.
68+
- [Sitemap](https://docs.starknet.io/sitemap.xml): Canonical crawl URL inventory.

scripts/validate_llms_txt.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Validate llms.txt format and local link integrity.
4+
5+
Rules:
6+
- First non-empty line must be a single H1.
7+
- Section headings must be H2 (## ...).
8+
- List items must use Markdown links.
9+
- Internal docs links must target docs.starknet.io and resolve to local .mdx pages.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import re
15+
import sys
16+
from collections import Counter
17+
from pathlib import Path
18+
from urllib.parse import unquote, urlparse
19+
20+
21+
REPO_ROOT = Path(__file__).resolve().parents[1]
22+
REPO_ROOT_RESOLVED = REPO_ROOT.resolve()
23+
LLMS_PATH = REPO_ROOT / "llms.txt"
24+
ALLOWED_NON_DOC_PATHS = {
25+
"/llms.txt",
26+
"/llms-full.txt",
27+
"/.well-known/llms.txt",
28+
"/.well-known/llms-full.txt",
29+
"/sitemap.xml",
30+
}
31+
ALLOWED_EXTERNAL_URLS = {
32+
"https://github.com/starkware-libs/starknet-specs/blob/master/api/starknet_api_openrpc.json",
33+
}
34+
ITEM_RE = re.compile(r"^- \[([^\]]+)\]\((https://[^\s)]+)\)(?:: .+)?$")
35+
36+
37+
def fail(message: str) -> None:
38+
print(f"ERROR: {message}", file=sys.stderr)
39+
sys.exit(1)
40+
41+
42+
def validate_canonical_path(path: str, url: str) -> None:
43+
if not path.startswith("/"):
44+
fail(f"URL path must be absolute: {url}")
45+
if "//" in path:
46+
fail(f"URL path must be canonical (no duplicate slashes): {url}")
47+
48+
segments = path.split("/")
49+
for segment in segments[1:]:
50+
if segment in {"", ".", ".."}:
51+
fail(f"URL path must be canonical (no dot-segments): {url}")
52+
53+
decoded_segment = unquote(segment)
54+
if decoded_segment in {".", ".."}:
55+
fail(f"URL path must be canonical (no encoded dot-segments): {url}")
56+
if "/" in decoded_segment:
57+
fail(f"URL path must be canonical (no encoded separators): {url}")
58+
59+
60+
def normalize_to_local_mdx(path: str) -> Path:
61+
"""
62+
Convert docs URL path to local file path.
63+
Example:
64+
/index.md -> index.mdx
65+
/build/quickstart/overview.md -> build/quickstart/overview.mdx
66+
"""
67+
relative = path.lstrip("/")
68+
if relative == "index.md":
69+
candidate = REPO_ROOT / "index.mdx"
70+
elif relative.endswith(".md"):
71+
candidate = REPO_ROOT / f"{relative[:-3]}.mdx"
72+
else:
73+
candidate = REPO_ROOT / relative
74+
75+
resolved = candidate.resolve(strict=False)
76+
if not resolved.is_relative_to(REPO_ROOT_RESOLVED):
77+
fail(f"URL path escapes repository root: {path}")
78+
return resolved
79+
80+
81+
def validate_headings(non_empty: list[tuple[int, str]]) -> int:
82+
h1_count = 0
83+
saw_h2 = False
84+
for line_no, line in non_empty:
85+
stripped = line.strip()
86+
if stripped.startswith("# "):
87+
if not stripped[2:].strip():
88+
fail(f"line {line_no}: H1 heading text cannot be empty")
89+
h1_count += 1
90+
elif stripped.startswith("## "):
91+
if not stripped[3:].strip():
92+
fail(f"line {line_no}: H2 heading text cannot be empty")
93+
saw_h2 = True
94+
elif stripped.startswith("#"):
95+
fail(f"line {line_no}: only H1/H2 headings are allowed, got: {stripped[:40]!r}")
96+
elif stripped.startswith("- "):
97+
pass
98+
elif not saw_h2:
99+
# Allow short prose intro only before the first section heading.
100+
pass
101+
else:
102+
fail(
103+
f"line {line_no}: unexpected line (not H1, H2, or list item): {stripped[:40]!r}"
104+
)
105+
106+
if h1_count != 1:
107+
fail(f"llms.txt must contain exactly one H1 heading, found {h1_count}")
108+
109+
section_count = sum(1 for _, line in non_empty if line.strip().startswith("## "))
110+
if section_count < 4:
111+
fail(
112+
f"llms.txt should contain multiple curated sections (found {section_count}, expected >= 4)"
113+
)
114+
115+
return section_count
116+
117+
118+
def parse_list_items(lines: list[str]) -> list[str]:
119+
urls: list[str] = []
120+
121+
for line_no, line in enumerate(lines, start=1):
122+
stripped = line.strip()
123+
if not stripped.startswith("- "):
124+
continue
125+
match = ITEM_RE.match(stripped)
126+
if not match:
127+
fail(
128+
f"line {line_no}: list entries must use '- [title](https://docs.starknet.io/...)[: description]'"
129+
)
130+
title, url = match.group(1), match.group(2)
131+
if not title.strip():
132+
fail(f"line {line_no}: link title cannot be empty")
133+
urls.append(url)
134+
135+
if len(urls) < 20:
136+
fail(f"llms.txt should be curated but substantial (found {len(urls)} links, expected >= 20)")
137+
138+
return urls
139+
140+
141+
def verify_urls(urls: list[str]) -> None:
142+
duplicates = sorted([u for u, count in Counter(urls).items() if count > 1])
143+
if duplicates:
144+
fail(f"duplicate URLs found: {duplicates}")
145+
146+
for url in urls:
147+
parsed = urlparse(url)
148+
if parsed.scheme != "https":
149+
fail(f"invalid URL scheme: {url}")
150+
if parsed.query or parsed.fragment:
151+
fail(f"URL must not include query parameters or fragments: {url}")
152+
153+
if parsed.netloc == "docs.starknet.io":
154+
validate_canonical_path(parsed.path, url)
155+
if parsed.path in ALLOWED_NON_DOC_PATHS:
156+
continue
157+
158+
if not parsed.path.endswith(".md"):
159+
fail(
160+
f"non-curated link must end in .md (or be allowlisted optional artifact): {url}"
161+
)
162+
163+
local_file = normalize_to_local_mdx(parsed.path)
164+
if not local_file.is_file():
165+
fail(f"URL does not map to a local .mdx file: {url} -> {local_file}")
166+
continue
167+
168+
if url not in ALLOWED_EXTERNAL_URLS:
169+
fail(f"external URL is not in allowlist: {url}")
170+
171+
172+
def main() -> None:
173+
if not LLMS_PATH.exists():
174+
fail("llms.txt does not exist at repository root")
175+
176+
text = LLMS_PATH.read_text(encoding="utf-8")
177+
lines = text.splitlines()
178+
179+
non_empty = [(i + 1, line) for i, line in enumerate(lines) if line.strip()]
180+
if not non_empty:
181+
fail("llms.txt is empty")
182+
183+
first_line_no, first_line = non_empty[0]
184+
if not first_line.strip().startswith("# "):
185+
fail(f"first non-empty line ({first_line_no}) must be an H1 heading")
186+
187+
section_count = validate_headings(non_empty)
188+
urls = parse_list_items(lines)
189+
verify_urls(urls)
190+
191+
print(
192+
f"llms.txt validation passed: {len(urls)} links, {section_count} sections, no format/link errors."
193+
)
194+
195+
196+
if __name__ == "__main__":
197+
main()

0 commit comments

Comments
 (0)