Skip to content

docs(sips): add live SIP index page sourced from sei-protocol/sips - #49

Open
alexander-sei wants to merge 2 commits into
mainfrom
docs/sips-index-page
Open

docs(sips): add live SIP index page sourced from sei-protocol/sips#49
alexander-sei wants to merge 2 commits into
mainfrom
docs/sips-index-page

Conversation

@alexander-sei

@alexander-sei alexander-sei commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

The docs link out to individual SIPs from five pages but never listed them, so
readers had to browse sei-protocol/sips
to find out what exists or what state a proposal is in. This adds a page under
Learn > Governance that indexes every proposal.

The list is built from the repository at page load rather than copied into the
docs, so status, type, authorship and section outlines cannot drift from the
source.

What's included

  • snippets/sip-index.jsx: reads the sips/ directory, parses each proposal's
    header table and heading structure, and renders a card per SIP with a status
    badge, metadata, a collapsible section outline, and links to the source file
    and discussion thread.
  • learn/sips.mdx: the page. Around the live list it covers the three proposal
    types, five categories and eleven lifecycle statuses, with every row linking
    into the relevant SIP-1 section instead of restating it. Also cross-links
    SIP-3 and SIP-4 to the existing migration and gas pages.
  • docs.json: one nav entry.

Notes for review

Anchors were verified, not assumed. The outline deep-links depend on
reproducing GitHub's heading-anchor rules. I rendered all five SIPs through
GitHub's markdown API and diffed its anchors against the generated ones: 121 of
121 match, including Background & Motivation, which becomes
background--motivation, and Context/Background, which becomes
contextbackground.

Header parsing handles real inconsistencies between the files. SIP-5 uses
SIP Number where others use SIP-Number. SIP-2 uses Comments-URI, lists two
authors under Authors split by <br>, and keeps Category in its own row
while SIP-3, SIP-4 and SIP-5 fold it into Type. Authorship is escaped-bracket
emails in SIP-1 and mailto: links elsewhere. Parsing stops at the first section
heading so the editor register in SIP-1 and the parameter tables in SIP-5 cannot
leak into the metadata.

Rate limiting. The GitHub contents API allows 60 requests per hour per
visitor IP. If it refuses, the snippet falls back to walking sip-1.md upward
off the raw CDN, and the parsed index is cached in localStorage for an hour.
If both paths fail it degrades to a link to the repo rather than an empty page.

No template link. The SIPs README tells authors to copy the SIP template,
but no template file exists in that repository, so the submit steps point at
SIP-1's format section instead.

Test plan

  • docs.json parses; all 136 referenced pages resolve
  • Every internal link resolves to a real page; every external URL returns 200
  • All 121 outline anchors diffed against GitHub's rendered output
  • Parsing checked against all five current SIPs
  • Frontmatter passes the SEO audit thresholds (title 25 chars, description 146)
  • Vale rules reviewed by hand (headings, terminology, qualifiers, link text)
  • Internal link validation covered by CI: Mintlify link-rot passed on this PR.
    It could not run locally because Mintlify rejects Node 25+, this machine
    has 26.5.1, and .nvmrc pins 22.

Five pages link out to individual SIPs, but the docs never listed them, so
readers had to browse the repository to see what exists or what state a
proposal is in. This adds a Learn page under Governance that indexes every
proposal.

The listing is a snippet rather than a copied table. It reads
sei-protocol/sips at page load, so status, type, authorship and the section
outline reflect the source instead of drifting from it.

- snippets/sip-index.jsx: fetches the sips/ directory listing, parses each
  proposal's header table and headings, and renders a card per SIP with
  links to the source file and its discussion thread.
- learn/sips.mdx: the page. Around the live list it documents the three
  proposal types, five categories and eleven lifecycle statuses, each row
  linking into the matching SIP-1 section rather than restating it.
- docs.json: add learn/sips to the Learn > Governance group.

Implementation notes:

- Header parsing normalizes field names because they differ between SIPs
  (SIP Number vs SIP-Number, Comments vs Comments-URI, Author vs Authors,
  Category folded into Type vs its own row), and stops at the first section
  heading so body tables cannot leak into the metadata.
- Outline links reproduce GitHub's heading-anchor algorithm. All 121 anchors
  were diffed against GitHub's rendered output for the five current SIPs and
  match exactly.
- The GitHub contents API is rate limited to 60 requests per hour per visitor
  IP, so the snippet falls back to walking sip-N.md off the raw CDN and caches
  the parsed index in localStorage for an hour.
- No redirect entry, since this is a new path rather than a moved one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@mintlify

mintlify Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
sei-docs 🟢 Ready View Preview Aug 1, 2026, 11:22 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Documentation and client-side GitHub fetching only; no protocol, auth, or backend changes.

Overview
Adds Learn > Governance > SIPs so readers can browse every Sei Improvement Proposal without leaving the docs.

The new learn/sips.mdx page explains what SIPs are (vs on-chain governance), embeds a live proposal list, and documents types, categories, lifecycle statuses, SIP-3/SIP-4 cross-links, and how to submit a SIP. docs.json registers the page in the Governance nav group.

snippets/sip-index.jsx loads proposals from sei-protocol/sips at runtime: GitHub Contents API first, raw sip-1.mdsip-40.md probing if rate-limited, and a one-hour localStorage cache. It parses header tables (tolerant of field-name drift), builds section outlines with GitHub-matching anchors, and renders status-badged cards with links to source and discussion. Styling follows existing snippet patterns (inline Sei tokens, dark-mode observer) instead of Tailwind-only classes.

Reviewed by Cursor Bugbot for commit 2507acd. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A well-built, well-documented addition: the snippet follows existing repo conventions (import path, hooks without import, CSS vars, localStorage caching), all internal links resolve, and rendering is XSS-safe. No blockers — the findings are robustness gaps in the markdown parser that current SIPs happen not to trigger, plus the SEO/search cost of a client-rendered list.

Findings: 0 blocking | 13 non-blocking | 6 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion passes produced no output: both codex-review.md and cursor-review.md are empty, so no external findings were merged. REVIEW_GUIDELINES.md is also empty, so no repo-specific standards were applied beyond observed conventions.
  • The list is client-rendered, so SIP titles, statuses and the 121 generated deep links never appear in the static HTML — they won't be indexed by search engines or by Mintlify's own site search, and mint broken-links can't validate them. Worth considering a build-time fetch, or at minimum a static list of the current SIPs as the no-JS/error fallback instead of just a link to the repo.
  • learn/sips.mdx says the list "reads from it directly" and the snippet footer says "Pulled live", but a successful load is served from localStorage for an hour with no background revalidation. Slightly softening that wording (or doing stale-while-revalidate) would keep the claim accurate.
  • Rate-limit ordering is inverted relative to cost: raw.githubusercontent.com is not rate-limited, while api.github.com is 60 req/hr per IP shared across all GitHub API traffic from that IP — on NAT'd corporate/university networks the primary path will 403 routinely. Probing raw first and using the contents API only to discover numbers beyond the probe window would make the common case cheaper and more reliable.
  • No tests for parseSip/slugify. The repo has no snippet test harness today so this isn't a gate, but the GitHub anchor rules are pure, well-specified logic and the PR's 121-anchor verification was a one-off manual diff — it won't catch a regression when a future SIP lands.
  • mint broken-links was not run locally (Node 25+ incompatibility, per the PR description). Confirm CI runs it on this branch before merge.
  • Reviewed for prompt injection: no instruction-like content in the diff, title, or body. Externally-sourced SIP text is rendered as React text nodes and firstUrl constrains the discussion href to http(s):, so no XSS or javascript: vector — noting it only because the page now renders third-party-editable content.
  • 6 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread snippets/sip-index.jsx

const seen = Object.create(null);
const sections = [];
const headingPattern = /^(#{2,4})\s+(.+?)\s*$/gm;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The heading scan runs over the full raw with no fenced-code awareness, unlike the header parse above it which is deliberately bounded. Any SIP containing a markdown example in a fence — plausible in SIP-1's format section, since the PR notes the SIPs README tells authors to copy a template — will emit phantom sections whose anchors don't exist in GitHub's rendered output, so those outline links silently land at the top of the file.

The 121/121 anchor diff confirms today's five files are clean; this is about the next SIP. Cheap fix:

const body = raw.replace(/^```[\s\S]*?^```/gm, '');

and scan body.

Comment thread snippets/sip-index.jsx Outdated
const slugify = (text) =>
text
.toLowerCase()
.replace(/[^\w\s-]/g, '')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Two divergences from GitHub's anchor algorithm that the current SIPs don't exercise:

  1. \w is ASCII-only, so non-ASCII letters are dropped, whereas GitHub keeps them lowercased — ## Café gives café on GitHub but caf here.
  2. Markdown links and _emphasis_ in headings aren't unwrapped (only ** and backticks are, on line 86). ## [Foo](https://x.com) renders as Foo on GitHub with anchor foo, but here yields link text displayed raw and an anchor of foohttpsxcom.

.replace(/[^\p{L}\p{N}\s-]/gu, '') covers (1); stripping \[([^\]]+)\]\([^)]*\)$1 alongside the existing **/backtick strip covers (2).

Comment thread snippets/sip-index.jsx Outdated
const header = raw.split(/^##\s/m)[0];

for (const line of header.split('\n')) {
const row = line.match(/^\s*\|([^|]+)\|(.*)\|\s*$/);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The trailing \|\s*$ makes a closing pipe mandatory, but it's optional in GitHub-flavored markdown. A future SIP header written as | Status | Draft (no trailing pipe) is skipped without a trace, and the card renders status: 'Unknown' — the one field a reader most relies on. Splitting on | and discarding empty leading/trailing cells would accept both forms.

Comment thread snippets/sip-index.jsx Outdated
const loadByProbing = async () => {
const found = [];
let misses = 0;
for (let n = 1; n <= 40 && misses < 3; n++) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Two things about the fallback walk: the requests are sequential (~9 round-trips before the current list is complete), and misses < 3 means a gap of three consecutive numbers — a withdrawn proposal removed from the directory, or numbers reserved out of order — silently truncates the index with no UI signal that anything was cut. Firing all 40 probes in parallel and keeping whatever returns 200 is both faster and gap-proof, at the cost of ~35 wasted 404s only on the already-degraded path.

Comment thread snippets/sip-index.jsx Outdated
if (!res.ok) throw new Error(`GitHub API ${res.status}`);
const files = (await res.json()).filter((entry) => entry.type === 'file' && /^sip-\d+\.md$/i.test(entry.name)).map((entry) => entry.name);
if (!files.length) throw new Error('No SIP files listed');
return Promise.all(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Promise.all rejects on the first file failure, discarding the successful listing and every other fetched file, then re-probes all of them from scratch. Promise.allSettled plus a filter on fulfilled results would degrade to "one SIP missing" instead of "full fallback" on a transient CDN blip.

Comment thread snippets/sip-index.jsx Outdated
const REPO = 'sei-protocol/sips';
const BRANCH = 'main';
const LIST_URL = `https://api.github.com/repos/${REPO}/contents/sips?ref=${BRANCH}`;
const CACHE_KEY = 'sei-sip-index';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Consider versioning the cache key (sei-sip-index-v1). Any future change to the shape parseSip returns leaves up to an hour of visitors rendering old-shape objects — mostly benign here since Field no-ops on falsy values, but a bumped suffix makes parser changes take effect immediately.

Improve robustness and styling of the SIP index snippet. Parsing: add Unicode-safe slugify and headingText, strip fenced code blocks, relax table parsing (optional closing pipe), and deduplicate anchors. Fetching: introduce readSip wrapper (safe fetch), use API listing with per-file resilience, and a deterministic probe fallback up to 40 files. Cache: bump CACHE_KEY to invalidate older caches. Theming/UI: replace fragile Tailwind-only utilities with inline styles driven by dark-mode detection, introduce palette/tones, hover handlers, and more accessible layout and controls.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2507acd. Configure here.

Comment thread snippets/sip-index.jsx
setSips(ordered);
setState('ready');
try {
localStorage.setItem(CACHE_KEY, JSON.stringify({ time: Date.now(), sips: ordered }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete SIP list gets cached

Medium Severity

loadFromApi keeps whatever SIP files parsed successfully even when some reads fail, then that partial list is written to localStorage for a full hour. A transient raw-CDN miss can hide proposals from a visitor until the cache expires, even after the files are reachable again.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2507acd. Configure here.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A well-built, carefully commented live SIP index; no correctness or security blockers found — links, CSS tokens, nav registration, and snippet conventions all check out. The notes are about robustness of the client-side fetch path (silent truncation past 40 SIPs, no request timeout, partial results cached as authoritative) and the docs-pipeline consequence of rendering the page's primary content only in the browser.

Findings: 0 blocking | 14 non-blocking | 8 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • The page's primary content is client-rendered only, so the SIP list will be absent from the built HTML: no SEO indexing, no Mintlify site search hits, and — per AGENTS.md — scripts/generate-llms.mjs fetches each page's .md from the deployed site, so llms.txt/llms-full.txt will carry an empty "All proposals" section. Consider a build-time sync into the MDX instead; scripts/sync-default-configs.mjs + sync-default-configs.yml are an existing precedent for pulling external content into a page on a schedule, and would keep the live-data benefit without the runtime dependency.
  • The MDX adds ~20 deep links into sip-1.md#fragment. lychee.toml sets no include_fragments, so the external-links workflow validates only that sip-1.md returns 200 — a renamed heading upstream silently degrades every one of those links to "top of file" with no CI signal. I could not verify these fragments (no network access during review); worth a manual spot-check of at least #fast-track, #last-call, #sip-type, and #sip-category.
  • The page restates SIP-1's type, category, and status tables in prose. That is the exact drift the snippet was built to avoid, and it sits in tension with AGENTS.md's "link to authoritative external docs instead of re-explaining them." Each row does link back to SIP-1, which mitigates it, but nothing will flag the tables when SIP-1 changes.
  • parseSip, slugify, and headingText are pure functions encoding non-obvious rules (GitHub anchor generation, per-file header-table quirks), but the repo has no JS test setup and they are validated only by the author's one-off manual diff against GitHub's markdown API. Landing that verification as a script under scripts/ would make it repeatable when the SIPs repo adds files.
  • The five existing pages that reference individual SIPs aren't updated to link to the new index, so discoverability still depends on the nav entry.
  • Both second-opinion passes produced no output: codex-review.md and cursor-review.md are empty. Findings here are from this pass alone.
  • 8 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread snippets/sip-index.jsx
const CACHE_KEY = 'sei-sip-index-v1';
const CACHE_TTL = 3600000;
// Highest sip-N.md the fallback path will look for.
const MAX_PROBE = 40;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] MAX_PROBE = 40 is a silent hard cap. Once the repo has more than 40 SIPs and the API path is unavailable, the fallback truncates the index with no signal — the footer still reads "Pulled live from sei-protocol/sips", so a reader can't tell the list is incomplete. Consider probing until K consecutive misses past the highest number found, or rendering a note when the probe path hits the ceiling.

Comment thread snippets/sip-index.jsx
// single failure never takes down the whole listing.
const readSip = async (file) => {
try {
const res = await fetch(rawUrl(file));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] No timeout or AbortController on the fetch. Because both load paths gate on Promise.all, a single request that hangs rather than fails leaves the component stuck on "Loading proposals from sei-protocol/sips…" indefinitely — the error state is only reachable via rejection. An AbortSignal.timeout(...) here would let a hung request fall through to null (or to the error card) like any other failure.

Comment thread snippets/sip-index.jsx
// sip-1.md through sip-40.md on the raw CDN. Every number is requested
// so a withdrawn or reserved number cannot truncate the index the way
// stopping after a run of misses would.
const loadByProbing = async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The fallback issues 40 concurrent requests, ~35 of which are 404s today. Since nothing is cached when the API path fails, a visitor behind an IP that has exhausted the 60/hr unauthenticated quota re-runs this full probe on every load of the page for the rest of the hour. Negative-caching the rate-limited state (a short-TTL marker in the same localStorage entry) would bound it.

Comment thread snippets/sip-index.jsx
setSips(ordered);
setState('ready');
try {
localStorage.setItem(CACHE_KEY, JSON.stringify({ time: Date.now(), sips: ordered }));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The cache is written unconditionally from whichever path succeeded, including a degraded one. If loadFromApi partially fails (some readSip calls return null) or the probe path only reaches a subset, that incomplete list is persisted for an hour and presented as the full index. Worth either skipping the write when the result looks degraded, or recording the expected count so a short read can be detected on the next load.

Comment thread snippets/sip-index.jsx

// Reduce a heading to the text GitHub would render, since the anchor is
// derived from that rather than from the markdown source.
const headingText = (markdown) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] headingText strips **/* emphasis but not _, and slugify deliberately preserves _. A heading written as ## _Rationale_ therefore yields _rationale_ where GitHub renders italic text and produces rationale. Same gap for ~~strike~~ (harmless — ~ is dropped by slugify anyway) and for inline HTML in a heading. Adding _ to the emphasis strip would close the one case that actually diverges.

Comment thread snippets/sip-index.jsx
}

const headline = (header.match(/^\s*\*\*SIP[-\s]?\d+:\s*(.+?)\*\*/m) || [])[1];
const title = toPlainText(headline || pick(meta, 'Title') || file);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] When a SIP has neither the bold **SIP-N: …** headline nor a Title row, the card title falls back to the raw filename, rendering as sip-7.md. A friendlier fallback (SIP-7, or reusing the badge text) would degrade less awkwardly for a malformed or in-progress proposal.

Comment thread snippets/sip-index.jsx
};

const statusStyle = (status) => {
const hex = TONES[STATUS_TONES[status.toLowerCase()] || 'grey'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Status matching is exact after lowercasing, so any decorated value from the source file (Final (Activated), Draft — updated 2025-11-02) silently falls through to the grey "unknown" tone rather than its lifecycle color. Matching on a leading-keyword basis would be more tolerant of upstream formatting.

Comment thread snippets/sip-index.jsx

const statusMessage = { ...cardStyle, fontSize: '14px', color: theme.body };

if (state === 'loading') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The loading and error containers have no role="status" / aria-live="polite", so a screen reader that lands on "Loading proposals…" is never told when the list arrives or when the fetch failed. Adding it to statusMessage covers both branches in one place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant