From 0b340bcaac16d232923878ece0ad975baecd37cb Mon Sep 17 00:00:00 2001 From: vibhusharma101 Date: Wed, 8 Jul 2026 21:50:54 +0530 Subject: [PATCH 1/2] feat: author bespoke HTML on the fly (freeform mode) + self-contained validator Every explainer was capped by the same 12-block template + 4 preset skins, so outputs only ever differed by color. This makes the AI author a complete, unique interactive HTML per subject instead. - Freeform is now the default: core/instructions/FREEFORM.md guides the AI to build a full self-contained page whose interactive centerpiece models the subject's real logic (real constants/thresholds/states), with a hard variety mandate so no two outputs look alike. - core/build/check-html.mjs: zero-dep gate that rejects any external resource load (external + + diff --git a/examples/novus-rate-limit.freeform.html b/examples/novus-rate-limit.freeform.html new file mode 100644 index 0000000..1230b3b --- /dev/null +++ b/examples/novus-rate-limit.freeform.html @@ -0,0 +1,404 @@ + + + + + + +The Window — How Novus Rate-Limits Its AI Endpoints + + + +
+
Novus · rate limiter
+ src/lib/rate-limit.ts + ● ALLOW +
+ +
+
Interactive systems walkthrough
+

Thirty requests a minute. Then the window says no.

+

Every paid AI call on Novus first passes one gate: a per-user counter that allows 30 requests every 60 seconds, resets on the clock, and — surprisingly — lets you through when it breaks. Below is a live model of that exact gate. Drive it.

+
+ limit = 30 + window = 60s + store = atomic Postgres counter + on error = fail open +
+
+ +
+

The gate, live

+

Fire requests and watch the current window fill. At 30 it flips to 429. When the 60-second window rolls over, the count resets to zero and you're clear again. The clock runs fast so you can watch it — speed is yours to set.

+ +
+
+ + check_ai_rate_limit(user, 30, 60) + t = 0.0s · window #0 +
+ +
+
+
+ + + + + +
0of 30 used
+
+
+
60.0s left in window
+
+
+
+ +
+
+

Requests per 60s window

current: #0
+
limit 30
+
+
+

Request log

+
No requests yet — hit “Send request”.
+
+
+
+ +
+ + +
rate 6/s
+ + +
Simulate infra down
+
+
+ allowed 0 + blocked 0 + windows elapsed 0 +
+
+
+ +
+

Two design decisions worth understanding

+
+
+
🪟
+

Fixed-window counting

+

Time is chopped into fixed 60-second windows. Each request bumps a counter for the current window; cross 30 and you're blocked until the clock ticks into the next window, where the counter starts fresh at zero. Simple, cheap, and atomic in Postgres.

+ watch the counter reset on rollover +
+
+
🚪
+

Fail open, on purpose

+

If the counter’s database call errors — say the migration hasn’t run yet — the limiter returns true and lets the request through. It’s best-effort hardening that must never take the app down. Errors aren’t attacker-controllable, so fail-open isn’t a bypass. Flip “infra down” to see it.

+ availability > strictness +
+
+
+ +
+

The whole limiter is this small

+
+
src/lib/rate-limit.ts
+
export async function checkRateLimit(
+  userId: string, limit = 30, windowSeconds = 60,
+): Promise<boolean> {
+  try {
+    const { data, error } = await supabaseAdmin.rpc('check_ai_rate_limit', {
+      p_user: userId, p_limit: limit, p_window_seconds: windowSeconds,
+    })
+    if (error) return true   // ← fail OPEN: infra error must not take the app down
+    return data === true
+  } catch {
+    return true          // ← same story: allow rather than crash
+  }
+}
+
+ +
+
+
The insight
+
A fixed window is the simplest limiter that still holds across serverless instances — one atomic counter, no memory. Its known cost is the window-edge burst: 30 near the end of one window plus 30 at the start of the next means ~60 land in seconds. Novus accepts that, and chooses to fail open, because the limiter is a guardrail on cost — not a security boundary.
+
+
+
+ +
+

Recap

+ +
+ + + + + + diff --git a/test/smoke.test.mjs b/test/smoke.test.mjs index fab55b5..cca7c67 100644 --- a/test/smoke.test.mjs +++ b/test/smoke.test.mjs @@ -14,6 +14,7 @@ import { readFileSync, readdirSync } from 'fs'; import { resolve, dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { buildHtml, validateAnalysis, KNOWN_BLOCKS } from '../core/build/inline.mjs'; +import { checkSelfContained } from '../core/build/check-html.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, '..'); @@ -78,6 +79,73 @@ for (const file of analysisFiles) { }); } +// ── 2b: at least one example exercises a bespoke `design` theme, and it stays self-contained ── +check('a bespoke design theme builds self-contained', () => { + const withDesign = analysisFiles + .map(f => JSON.parse(readFileSync(join(EXAMPLES, f), 'utf-8'))) + .find(a => a.design && typeof a.design === 'object'); + assert(withDesign, 'no example carries a `design` theme to exercise the feature'); + assert(withDesign.design.vars && typeof withDesign.design.vars === 'object', 'design.vars should be an object'); + + const html = buildHtml(withDesign); + for (const token of ['{{STYLES}}', '{{APP_JS}}', '{{ANALYSIS_JSON}}']) { + assert(!html.includes(token), `unsubstituted placeholder ${token} left in output`); + } + const external = (html.match(/(?:src|href)\s*=\s*["']https?:\/\/[^"']+/gi) || []) + .filter(ref => !/github\.com/i.test(ref)); + assert(external.length === 0, `design theme introduced external reference(s): ${external.join(', ')}`); +}); + +// ── 2c: malformed `design` shapes are rejected ── +check('rejects non-object design', () => { + let threw = false; + try { validateAnalysis({ schemaVersion: 2, meta: {}, blocks: [{ type: 'hook' }], design: 'nope' }); } + catch { threw = true; } + assert(threw, 'accepted a string design'); +}); +check('rejects non-string design.css', () => { + let threw = false; + try { validateAnalysis({ schemaVersion: 2, meta: {}, blocks: [{ type: 'hook' }], design: { css: 42 } }); } + catch { threw = true; } + assert(threw, 'accepted a numeric design.css'); +}); + +// ── 2d: freeform mode — every committed examples/*.freeform.html is self-contained ── +const freeformFiles = readdirSync(EXAMPLES).filter(f => f.endsWith('.freeform.html')); +if (freeformFiles.length > 0) { + console.log(`Freeform examples (${freeformFiles.length}):`); + for (const file of freeformFiles) { + check(`${file} — self-contained`, () => { + const html = readFileSync(join(EXAMPLES, file), 'utf-8'); + const { ok, errors } = checkSelfContained(html); + assert(ok, `not self-contained: ${errors.join('; ')}`); + }); + } +} + +// ── 2e: the self-contained checker actually rejects external references ── +console.log('Freeform validator guards:'); +check('accepts a minimal self-contained doc', () => { + const html = ''; + assert(checkSelfContained(html).ok, 'rejected a valid self-contained doc'); +}); +check('rejects external '; + assert(!checkSelfContained(html).ok, 'accepted an external script'); +}); +check('rejects external stylesheet ', () => { + const html = ''; + assert(!checkSelfContained(html).ok, 'accepted an external stylesheet'); +}); +check('rejects remote CSS url()', () => { + const html = ''; + assert(!checkSelfContained(html).ok, 'accepted a remote CSS url()'); +}); +check('allows an hyperlink (navigation, not a resource load)', () => { + const html = 'link'; + assert(checkSelfContained(html).ok, 'wrongly rejected an hyperlink'); +}); + // ── 3: negative test — the validator must reject bad input ── console.log('Validator guards:'); check('rejects wrong schemaVersion', () => { From 87d546a279dc9e7334898941f68a15b8e1f16194 Mon Sep 17 00:00:00 2001 From: vibhusharma101 Date: Fri, 10 Jul 2026 20:51:02 +0530 Subject: [PATCH 2/2] fix: sync CLI + docs with freeform mode, add check command, showcase examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI (bin/cli.mjs) was still entirely on the pre-freeform blocks-only flow: `instructions` and the adapters written by `init` never mentioned FREEFORM.md, so a fresh `npx think-in-html init` would not actually deliver the new default bespoke-HTML generation added in the freeform-mode work — it would silently fall back to the legacy templated engine. This fixes the gap before publish. - bin/cli.mjs: `instructions` now prints FREEFORM.md + mode guidance by default; `--blocks` opts into the legacy ANALYZE.md + schema flow. Added a `check ` command wrapping check-html.mjs's self-contained validator. Updated the Claude Code / Cursor / Codex adapter templates written by `init` to describe the freeform-first flow, with `--blocks` as the explicit legacy path. - package.json: ship `examples/` in the npm tarball (FREEFORM.md references examples/*.freeform.html as a study reference) and refresh the description. PUBLISHING.md's expected `npm pack --dry-run` file list updated to match. - examples/index.html: the two freeform reference examples (pricing-desk, rate-limit) were generated but never added to the live gallery — added cards for both so the headline feature is actually visible when people click through from the README. - README.md: leads with bespoke/freeform generation, documents the `check` command and --blocks legacy flag, links both freeform gallery examples, and adds a placeholder for the future landing-page/waitlist link. - .gitignore: ignore a local-only LANDING-PLAN.md planning doc. Verified: npm test (18/18), `npm pack --dry-run` includes examples/, and `think-in-html check` correctly passes a good file and rejects a crafted file with an external