From 74d60cadddaf9583afa4d6ab74525aa562a3b51b Mon Sep 17 00:00:00 2001 From: Abdelrahman Essawy Date: Sat, 29 Aug 2026 20:02:06 +0300 Subject: [PATCH] fix(limits): correct the published rate limit and file size, check descriptions The limits page advertised "30 vs 300/min" and "100 MB vs 2 GB" in its frontmatter description while its table, which is checked, said 120/600 and 500 MB/10 GB. The description understated the rate limit 4x and the file size 5x, and it is the text a search result and an AI answer quote. check-plan-limits.mjs only read tables. It now reads descriptions too, and each pattern must match: rephrase the sentence and it fails asking you to update the check, rather than quietly checking nothing. Two more checks were already dead and this surfaced them. "max batch size" matched no row because batches were removed from the product, and the storageQuota row was keyed on "total r2 storage" while the heading reads "Total object storage", so it had been silently unverified. --- scripts/check-plan-limits.mjs | 64 +++++++++++++++++++++++++++++++++-- support/limits.mdx | 2 +- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/scripts/check-plan-limits.mjs b/scripts/check-plan-limits.mjs index 66591a0..5e9b9ea 100644 --- a/scripts/check-plan-limits.mjs +++ b/scripts/check-plan-limits.mjs @@ -45,14 +45,44 @@ const ROWS = [ ["queued backlog", "maxQueuedJobs", fmtInt], ["api requests", "apiRequestsPerMinute", fmtInt], ["max input file", "maxInputFileSize", fmtBytes], - ["max batch size", "maxBatchSize", fmtInt], ["job timeout", "maxJobTimeout", fmtDuration], ["output retention", "outputRetentionDays", fmtDays], - ["total r2 storage", "storageQuota", fmtBytes], + // The table heading is "Total object storage"; the old "total r2 storage" + // label matched nothing, so this row went unchecked. + ["total object storage", "storageQuota", fmtBytes], ]; const FILES = ["concepts/credits.mdx", "support/limits.mdx"]; +/** + * Frontmatter descriptions repeat these numbers, and nothing checked them. + * support/limits.mdx advertised "30 vs 300/min" and "100 MB vs 2 GB" while its + * table, which IS checked, said 120/600 and 500 MB/10 GB. The description is + * what a search result and an AI answer quote, so it was the most-read wrong + * number on the site. + * + * Each pattern must match. Rephrase the sentence and this fails, telling you to + * update the pattern, rather than quietly checking nothing. + */ +const DESCRIPTION_CLAIMS = [ + { + file: "support/limits.mdx", + what: "rate limits", + pattern: /rate limits \((\S+) vs (\S+)\/min\)/, + key: "apiRequestsPerMinute", + fmt: fmtInt, + }, + { + file: "support/limits.mdx", + what: "file size caps", + pattern: /file size caps \(([\d.]+ [MG]B) vs ([\d.]+ [MG]B)\)/, + key: "maxInputFileSize", + fmt: fmtBytes, + }, +]; + +const description = (md) => /^description:\s*"([^"]*)"/m.exec(md)?.[1] ?? ""; + /** Find the `| label… | free | pro |` row and return the free/pro cells. */ function tierRow(md, labelSubstring) { const line = md @@ -75,11 +105,15 @@ async function main() { } const errors = []; + // A label that matches nothing used to `continue` silently, so renaming a row + // heading disabled its check without a word. Track what matched instead. + const matchedLabels = new Set(); for (const file of FILES) { const md = readFileSync(join(ROOT, file), "utf8"); for (const [label, key, fmt] of ROWS) { const row = tierRow(md, label); if (!row) continue; // not every row is in every file + matchedLabels.add(label); const wantFree = fmt(source.plans.free.limits[key]); const wantPro = fmt(source.plans.pro.limits[key]); if (row.free !== wantFree || row.pro !== wantPro) { @@ -90,13 +124,37 @@ async function main() { } } + for (const label of ROWS.map(([l]) => l)) { + if (!matchedLabels.has(label)) { + errors.push(`no table row matched "${label}" in any checked file — was the row renamed or removed?`); + } + } + + for (const claim of DESCRIPTION_CLAIMS) { + const desc = description(readFileSync(join(ROOT, claim.file), "utf8")); + const m = claim.pattern.exec(desc); + if (!m) { + errors.push( + `${claim.file}: the description no longer states "${claim.what}" in the expected shape, so it is unchecked. Update DESCRIPTION_CLAIMS.`, + ); + continue; + } + const wantFree = claim.fmt(source.plans.free.limits[claim.key]); + const wantPro = claim.fmt(source.plans.pro.limits[claim.key]); + if (m[1] !== wantFree || m[2] !== wantPro) { + errors.push( + `${claim.file}: description says ${claim.what} are ${m[1]} vs ${m[2]} but source says ${wantFree} vs ${wantPro}`, + ); + } + } + if (errors.length) { console.error("✗ Plan-limit tables drifted from the source of truth:\n" + errors.map((e) => ` - ${e}`).join("\n")); console.error(`\nUpdate the tables to match ${SOURCE_URL} (edit price-tiers.ts in the monorepo to change a value).`); process.exitCode = 1; return; } - console.log("✓ Plan-limit tables match the source of truth."); + console.log("✓ Plan-limit tables and descriptions match the source of truth."); } main(); diff --git a/support/limits.mdx b/support/limits.mdx index 4f2818f..209b6d5 100644 --- a/support/limits.mdx +++ b/support/limits.mdx @@ -1,7 +1,7 @@ --- title: "Plan limits and quotas" sidebarTitle: "Limits" -description: "Compare Free and Pro plan limits across rate limits (30 vs 300/min), file size caps (100 MB vs 2 GB), retention, concurrency, storage." +description: "Compare Free and Pro plan limits across rate limits (120 vs 600/min), file size caps (500 MB vs 10 GB), retention, concurrency, storage." icon: "gauge" keywords: ["rendobar limits", "api rate limits", "plan limits", "file size limit", "concurrent jobs"] canonical: "https://rendobar.com/docs/support/limits"