* Add Mini Course Generator to adopters list
Added a new card for the Mini Course Generator showcasing its AI-powered course generation capabilities.
* docs(adopters): add Mini Course Generator
---------
Co-authored-by: eren-commits <eren@minicoursegenerator.com>
The layout audit fingerprints each element by rect and opacity, plus an
8x8 pixel hash for canvas and video. A composition whose only change over
the timeline is font-variation-settings touches none of those, so all six
samples hash identically and the audit reports sweep_static on a timeline
that is visibly animating.
Duplexed variable fonts make it unavoidable rather than unlikely. Recursive
holds one advance width across its whole weight axis by design, so even the
element rect stays fixed while the letterforms change.
Fold the computed font-variation-settings into the per-element fingerprint.
It reads normal on every element that does not use the property, so no
existing composition's fingerprint changes.
This is the third instance of this failure class in the same function,
after the canvas-repaint and opacity-reveal cases whose regression tests
already sit alongside these. Two more join them: a scene where only the
axes move must change the fingerprint, and a scene where nothing moves
must hash identically twice. The second is the load-bearing one, since a
fingerprint that varies on its own would make sweep_static unfireable and
every green layout verdict meaningless.
A search miss already reached PostHog, where nobody was watching it. It now
also goes to the same channel a CLI rating goes to, by the same best-effort
POST, so a move the catalog is missing is read rather than queried.
The forward is bounded and swallowed, and the ack prints before it, so a gap
report can never fail or delay the command that sent it.
#3516 merged from a branch cut before da6514d45 and brought back
`expect(document).not.toHaveProperty("modelContext")`, along with the old
test title. This re-applies that fix.
The assertion is racy, not merely wrong. The hook awaits a dynamic import
of the `@mcp-b/global` polyfill inside its mount effect, so whether
`document.modelContext` exists when the assertion runs depends only on
whether that import has resolved yet. `await act()` usually returns first,
which is why main went green while another PR's run went red. Inserting a
100ms wait before the assertion on the current tip makes it fail every
time, and the failure dump shows all five tools registered through the
polyfill: the real behaviour is the opposite of what the line asserted.
Installing `document.modelContext` where the browser has none is the
polyfill's job. What the case is really for is that mounting does not
throw, so a browser without native WebMCP still boots Studio.
* fix(cli): persist what the server answered for a captured page
A page that renders is not a page that succeeded. An error page has a title,
a palette, typefaces and a DOM, so every extractor downstream reads it happily
and produces a design system belonging to whoever wrote the error page rather
than to the site's owner.
The status was already read, once, to feed `detectBlockedPage`, and then
dropped. That helper cannot stand in for it: it decides whether the rendered
document LOOKS like a protection wall, over a minimal-DOM heuristic that a
rich error page passes. "Was this blocked?" and "what did the server answer?"
are two questions, and widening the first to carry the second would leave a
heuristic owning a fact.
So the response status is persisted plainly, as its own record, and every
consumer decides for itself what a non-success response means for its product.
Written before the blocked-page check runs, so the record's absence means
"navigation never produced a response" — a third state distinct from a status
of 404 and from a status of null.
* fix(cli): report the response status in the capture command's JSON output
The status reached `CaptureResult` and was then dropped at the CLI boundary,
which is the same read-once-and-discard that made the error page harvestable
in the first place. `--json` is the documented programmatic surface, and an
agent reading `ok: true` off a capture of a 404 has no way to see it there.
Bare `hyperframes skills` passed `--skill '*'` to the upstream installer, which discovers every SKILL.md in the clone, so a full install put 26 skills on disk against the 20 the manifest publishes (the six repo-internal ones under .claude/skills / .agents/skills). It now resolves the canonical manifest and installs its names, the same path `skills update` uses; offline it warns and degrades to the pinned core set instead of the wildcard. Docs point the full set at `npx hyperframes skills` and say what `skills add --all` really pulls.
Rebuilding footage means checking the render against the source at the same timestamps. `--against <video>` saves the reference frame at every captured time (frame-exact seek: -ss after -i) plus a labelled render|reference pair sheet built with the existing contact-sheet helper. Reference frames and pair sheets are derived artifacts, kept out of the snapshot list like contact-sheet.jpg. No new dependencies.
* fix(fonts): make Google Fonts subsetting CSS text-transform aware
Extends the subset character closure to cover locale/context-sensitive
case transforms and non-case CSS text-transform values:
- Parse lang attributes from authored HTML and apply toLocaleUpperCase/
toLocaleLowerCase for each detected locale (covers Turkish İ/ı,
Azeri, German ẞ, and other locale-dependent casing)
- Map ASCII U+0021–U+007E to fullwidth equivalents U+FF01–U+FF5E when
full-width appears in the source
- Map small hiragana/katakana to full-size equivalents when
full-size-kana appears in the source
- Preserve the existing 1700-char encoded URL budget and full-font
fallback
Closes#3496
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: extract helpers to reduce complexity and duplication
Split extractGoogleFontsText into addCaseClosure, addFullwidthVariants,
and addFullSizeKanaVariants. Extract subsetTextFor test helper to
eliminate repeated URL→text boilerplate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: validate lang tags, scope transform gates, deduplicate closure input
- Validate lang attributes with Intl.getCanonicalLocales before passing
to toLocaleUpperCase — malformed tags (en_US, x, 123) no longer throw
RangeError.
- Gate fullwidth/kana expansion on text-transform declarations instead
of raw html.includes — a CSS class named .full-width no longer eats
half the URL budget.
- Deduplicate characters before the closure loop (new Set) to avoid
redundant locale conversions on base64-heavy compositions.
- Benchmark now asserts the transform cost delta, not just that one
small fixture fits under the cap.
- Restore over-approximation comment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prevent regex bridging across rules and add case-insensitive matching
Exclude {} from the text-transform regex character class so the match
cannot cross rule boundaries when the trailing semicolon is omitted.
Add /i flag so uppercase declarations (text-transform: FULL-WIDTH)
are not missed. Test now uses a fixture with both a text-transform
declaration and a .full-width class to exercise the bridging case.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove redundant \s* to eliminate quadratic backtracking
Drop the \s* between : and [^;{}]* — the character class already
matches whitespace, and the adjacent quantifiers caused quadratic
backtracking on inputs like "text-transform:" + " ".repeat(N).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace regex with linear indexOf/slice scan (js/polynomial-redos)
The text-transform regex backtracks O(n²) on input with many
text-transform: runs and no ;{} between them. Replace with a linear
indexOf/slice scan following the repo's existing pattern for this
CodeQL rule (beatFile.ts, utils.ts, parseStoryboard.ts).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agents were shipping <video src="….jpg"> / <img src="….mov"> because the existing URL-resolvable check stayed green. Report an error so the agent rewrites the tag; skip audio, <source>, and extensionless URLs.
* fix(core): catch PostCSS parse errors instead of dropping compositions
Invalid CSS in a sub-composition style block made postcss.parse throw
inside scopeCssToComposition. The throw propagated to the composition
loader's catch block, which emptied the host — silently dropping the
entire scene. Lint swallowed the same error via catch { continue },
reporting 0 warnings.
Two fixes:
- Runtime: wrap postcss.parse in try/catch and return the original
(unscoped) CSS on failure, so the composition still mounts
- Lint: emit a css_parse_error finding instead of silently continuing
Fixes#3585.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: drop unparseable CSS instead of leaking it unscoped
Return "" on PostCSS parse failure so sub-composition stylesheets
that cannot be scoped are dropped rather than injected unscoped into
the parent document. Updates test fixture to use valid+malformed CSS
that demonstrates the leak risk.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
declaredSize() matched the rel token "apple-touch-icon" exactly, so the
legacy "apple-touch-icon-precomposed" spelling (one token, not two) fell
through to 0 instead of the 180px Apple default, even though the selector
already collects it. Same page, one spelling apart, opposite winner inside
tier 1 — it never drops a candidate and never beats the .ico tier.
Switch to a startsWith check on the token. Exact-token matching stays for
mask-icon, where a longer rel really would be a different asset.