Toolcrib vs. Forge: A Head-to-Head, Not a Feature Comparison

What happens when you actually build the same app with @nexcraft/forge that “Why Toolcrib?” already built with toolcrib — following the documented setup, on the first attempt, with no adversarial input at all. Forge calls itself “AI-Native.” These are the four ways it fails that claim specifically.


Good evening. Some of you will recognize the shape of tonight’s story — you’ve seen it before, told slowly, over sixty days, in a different episode. Tonight it happens to the one holding the flashlight instead of the one in the dark room, and it happens all in one sitting: the same three acts, just compressed — confidence, a mounting count of things nobody warned about, and by the end, a quiet admission that even the investigator found it a little much.

1. What this paper is, and isn’t

“Why Toolcrib?” compared toolcrib and forge on paper — leverage channels, delivery models, test suites read live, manifest-generation methodology inspected line by line. All of that stands. What it didn’t do is what the original toolcrib experiments did: actually build the same real app with forge and drive it in a browser. This paper closes that gap.

The result changes the shape of the comparison. The earlier paper’s forge discussion was about tradeoffs — training-data leverage, repairability, framework-longevity positioning, real feature gaps worth naming honestly on both sides. This paper isn’t about tradeoffs. Following forge’s own documented quick-start, on the first attempt, building the most ordinary CRUD dashboard imaginable, produced four independent, severe defects — not stylistic disagreements, not narrower coverage, but components that don’t work at all, silently, exactly where a consumer would have no reason to suspect anything was wrong.

2. The setup

Same app spec as the original toolcrib experiment: a task/project dashboard — header, four stat cards, a toolbar (search, two filter selects, a new-task button), a sortable task table with a delete action, and a modal form with five fields and required-field validation. Same stack shape (Vite, React, TypeScript), same testing tool (Puppeteer against a local Chrome build, production vite preview build, not a dev server). @nexcraft/forge@0.10.0 and @nexcraft/forge-react@1.0.5 installed exactly as the README instructs, components imported exactly as the quick-start shows.

3. Finding one: nothing registers, and nothing tells you why

The build succeeds. The type-check passes. The page loads with zero console errors. And every single forge component — button, modal, select, data table, all of them — silently renders as an unstyled fallback, because not one custom element actually registered in the browser.

The cause is a direct contradiction sitting in the package’s own metadata: @nexcraft/forge‘s package.json declares "sideEffects": false, which tells a bundler “nothing in this package does anything just by being imported — drop what isn’t used.” But every forge component works exclusively through import-time side effects (customElements.define(...)); there’s no named export path that doesn’t route through that registration. Declaring the package side-effect-free while depending entirely on side effects to function is asking the bundler to remove the one thing that makes the library work, and Vite does exactly that, correctly, by its own contract.

It gets worse under inspection, not better. Forge ships per-component subpath imports specifically so consumers can register only what they use (@nexcraft/forge/button), and each one contains its own defensive check — a throw new Error('ForgeButton not found. Make sure @nexcraft/forge is properly loaded.') that the library’s own authors clearly wrote anticipating exactly this failure. That check depends on an inner side-effect import one level further in, which is tree-shaken away by the identical logic, one level deeper. The result is a library that shipped its own warning light for this exact scenario, wired to a circuit that the same misconfiguration disconnects before it can ever fire. It is not simply undetected — it is, by construction, undetectable through the mechanism the library provides for detecting it.

The fix — verified by patching sideEffects: true directly and watching the bundle nearly triple in size as real component code came back — is not available to an ordinary consumer without hand-patching node_modules, and it isn’t documented anywhere as a known caveat for Vite or any other tree-shaking bundler.

3a. What it actually took to find that fix

The escalation itself is a finding, separate from the bug it eventually uncovered. Getting from “the documented setup” to “a build with real components in it” took five distinct attempts, each one requiring bundler-internals knowledge no consumer should need to reach for building a CRUD dashboard:

  1. The documented barrel import (import '@nexcraft/forge') — failed silently. No error, no signal, just unregistered components.
  2. The documented “selective” per-component imports (import '@nexcraft/forge/button', etc.) — also failed silently, for the same reason one level removed.
  3. Forcing retention with void [...] on the imported bindings, on the theory that referencing them would stop the bundler from treating them as unused — failed. A void expression over bare variable references has no side effect Rollup can’t also optimize away, so the “fix” itself got tree-shaken.
  4. Forcing retention by assigning the imports to window — a genuine, unremovable side effect. This worked, in the narrow sense that it finally let one component’s own defensive code survive and run — which is how the library’s own error message was seen at all. It did not fix the underlying registration; it just proved the diagnosis.
  5. A Vite/Rollup config override (treeshake.moduleSideEffects) targeting the package by path — failed to change the output at all, for reasons that weren’t worth chasing further given a more direct option was available.
  6. Patching the installed package’s package.json directly (sideEffects: true) — this is what finally worked, confirmed by the bundle size change.

Six attempts, the last of which requires editing a third-party dependency’s metadata inside node_modules by hand — something no build process survives past a fresh npm install, and not a fix any consumer would arrive at without already suspecting a tree-shaking misconfiguration specifically, which nothing in the observed symptoms (clean build, clean type-check, silent runtime failure) points toward. This is exactly the population “Why Toolcrib?” spends a full section on: someone depending on an AI assistant for the entire loop, with no manual debugging step available at all, would not get a sixth attempt. They’d get a broken app and nothing telling them why, on the first and only attempt they were ever going to make.

3b. The same pattern repeats at every layer, and it’s worth counting once, in full

Finding 1 wasn’t the only place this happened — it was just the first. The same shape of escalation recurred at the data table and the modal, and the honest way to compare toolcrib and forge on this axis isn’t “forge has four bugs” — it’s how many attempts each toolkit needed before anything was left standing to test, versus how many toolcrib needed.

Toolcrib, from the original v0.5.0 experiment: toolcrib init, toolcrib apply — two commands, following the documented flow exactly, no deviation. The app worked on the first build. Four small, narrow gaps were found only because the app was already working well enough to drive a full accessibility battery against it — a missing id on a Select, an aria-invalid wire-up, a keyboard-sort gap, a default-prop mismatch. Every one of them had a concrete, mechanical, one-line-to-few-line proposed fix written the same session, with no open question about how to fix it, only whether it had been yet.

Forge, this report, counted in full:

LayerAttempts before anything workedFinal state
Component registration (Finding 1)6 (documented barrel import → documented subpath imports → forced-retention trick #1 → forced-retention trick #2 → Rollup config override → direct node_modules patch)Fixed, but only via an undocumented, non-persistent hand-patch
ForgeDataTable data (Finding 2)3 tried in this follow-up alone (rows prop added → id/label columns matching the component’s own docs → still broken); reproduced again natively, no wrapper at allUnresolved, confirmed as a core-component bug. Column headers render; body rows never do, behind a crash reproducible with zero wrapper involved
ForgeDatePicker (Finding 3)1 (swapped for a plain input to isolate Finding 4); crash reproduced again nativelyUnresolved, confirmed as a core-component bug. Identical crash, wrapper or no wrapper
ForgeModal (Finding 4)Tested through the wrapper (5/5 checks failed), then re-tested natively with no wrapper (3/5 checks passed)Partially resolved by bypassing the wrapper. Overlay rendering, dialog semantics, and Escape were wrapper bugs, now confirmed fixed by going native. Focus trap and backdrop-click-to-close are not — both still fail with zero wrapper involved

Toolcrib: two commands, works, four minor gaps found and fixed the same session. Forge: at minimum thirteen distinct attempts across three components and two separate test environments (through the wrapper, then natively), one dependency hand-patched directly — and even after giving forge the fairest possible second chance, three of the four original defects are still present in some form: two (DataTable, DatePicker) fully confirmed as forge’s own bugs, independent of anything React-related, and the third (Modal) improved from a total failure to a partial one, still short of what toolcrib passed cleanly. Toolcrib’s four gaps were the kind a working app surfaces under a thorough test. Forge’s are the kind that stop the app from being a working app to test in the first place — and giving forge every reasonable benefit of the doubt still leaves most of them standing.

4. Finding two: the table and the wrapper don’t speak the same language

Past the registration fix, ForgeDataTable renders — and shows “No data available” no matter what’s passed to it. The wrapper does successfully set a data property on the live element, three rows, correct shape, confirmed by reading it straight off the DOM. The underlying component, per its own shipped documentation, reads a property called rows. Reading that property directly off the same element confirms it: empty, untouched, the component’s own default.

This isn’t a subtle naming quibble discovered by close reading. The React wrapper’s own TypeScript types declare data and a key/title column shape; the web component’s own docs declare rows and an id/label column shape. Two halves of the same product, shipped together, disagree with each other about the interface between them, in a way that means the documented usage pattern cannot populate the table with any input at all.

A follow-up attempt tried to actually fix this rather than stop at diagnosing it — passing rows directly and switching to the documented id/label column shape. Column headers now render correctly. The table body still doesn’t — a third, distinct error appears (Cannot read properties of undefined (reading 'title')). §6b confirms this specific crash independently of any wrapper at all.

5. Finding three: an empty string crashes the app

The dashboard’s due-date field starts empty — an entirely unremarkable default for an unfilled date input. Passed to ForgeDatePicker, this throws this.value?.getTime is not a function during render: the component’s internal logic assumes its value is always a Date object and calls .getTime() on it with no guard, while its own documented prop type accepts a plain string. With no error boundary in place, the crash takes the whole React tree down with it.

6. Finding four: the modal isn’t a modal — through the wrapper

With the date-picker crash isolated and the registration fix applied, the “New Task” button was clicked with no runtime errors — and the form content appeared inline, in normal page flow, underneath the data table. No overlay. No backdrop. No role="dialog" anywhere. Running toolcrib’s original focus-trap/Escape/backdrop battery: every row failed.

6b. Giving forge a second chance: the native Web Components, no wrapper, no bundler at all

Three of these four findings trace through @nexcraft/forge-react specifically. The fair question is whether that’s a wrapper problem or a core-component problem — and the only honest way to answer it is to bypass the wrapper entirely: no React, no bundler, no build step, a plain HTML file loading forge’s own ESM bundle directly via a <script type="module"> tag and driving the native custom elements with their own documented API. A bare script-tag import can’t be tree-shaken by anything — this is as close to “forge, on its own, with every benefit of the doubt” as a test can get.

Registration: clean. All four custom elements (forge-modal, forge-data-table, forge-date-picker, forge-button) register correctly with zero bundler involved — fully consistent with Finding 1 being a downstream-bundler problem specifically, not a defect in the components themselves.

ForgeDataTable, tested with the exact rows/id/label shape its own docs specify: the identical crash reproduces, verbatim — Cannot read properties of undefined (reading 'title'), at the same line in the same bundle, with zero wrapper or bundler involved. This confirms Finding 2’s remaining failure is a genuine defect in forge’s own core component, not an artifact of the React wrapper. The second chance doesn’t rescue it.

ForgeDatePicker, given an empty string via its native property: the identical crash reproduces, verbatim — this.value?.getTime is not a function. Same conclusion: this is forge’s own bug, confirmed independent of any wrapper.

ForgeModal is where the second chance actually changes the picture. Opened natively (with forge’s own tokens.css loaded, to give it a fair, properly-themed test), it renders as a real overlay: a dimmed, full-page backdrop, a properly positioned dialog card, confirmed by screenshot. Inspecting the shadow DOM directly finds real dialog semantics — role="dialog", aria-modal="true", a proper aria-label. Escape correctly closes it. None of that was true through the wrapper, where all five checks failed outright.

But it isn’t a clean pass, either. Tabbing through it 30 times, focus still escapes the modal — the trap doesn’t hold, natively, the same as it didn’t through the wrapper. And clicking the backdrop, at a fixed coordinate well outside the dialog card, does not close it — the modal stays open. Two of the five checks toolcrib passed cleanly still fail in forge’s own, wrapper-free, best-case implementation.

Checktoolcribforge, through -reactforge, native
Renders as a distinct overlay✅❌✅
role="dialog" present✅❌✅
Escape closes it✅❌✅
Focus trap holds across 30 tabs✅❌❌
Backdrop click closes it✅❌❌

The honest verdict, giving forge every fair chance the wrapper denied it: the wrapper was actively making things worse for ForgeModal specifically — three of five guarantees the core component actually provides were being thrown away by @nexcraft/forge-react before they ever reached a consumer. That’s worth crediting plainly; it changes what Finding 4 is actually about. It doesn’t change the scoreboard against toolcrib, though. Even at its best, wrapper-free, properly themed, tested fairly — forge’s modal still fails two of the five checks toolcrib passed cleanly. And two of the four original findings (DataTable, DatePicker) don’t improve at all under the same fair test; they’re confirmed, independently, as forge’s own bugs.

6c. This specific finding, toolcrib already tests for on purpose

Worth being precise here rather than crediting toolcrib globally: toolcrib maintains a genuine, separate Playwright suite (test:e2e, real Chromium, confirmed directly by cloning and reading it) alongside its 587-test Vitest suite — and its own e2e/README.md states exactly why, in terms that describe this failure class almost exactly: “a component relying on Radix Presence waiting for [an animation to end] can look correct in jsdom while being permanently stuck open in a real browser.” That’s the toolcrib team naming, in their own words, the category of bug where something looks fine to a simulated DOM and isn’t. Given §6b’s native results, that gap maps onto forge’s remaining ForgeModal defects with real precision: the parts that fail even in forge’s own best-case, wrapper-free implementation — focus never trapped, backdrop click never closing it — are exactly the kind of thing a props-only or jsdom-only test would report as fine (the props exist, the classes are there) while a real browser, doing a real 30-tab sweep and a real click, would not.

Forge, checked directly, has no equivalent. Its own package.json shows a test:unit and test:a11y — both run through Vitest, both jsdom-based, no Playwright dependency anywhere in the repository. Its “accessibility” tests are the same simulated-DOM suite as everything else, filtered by test name, not a real browser at all. Toolcrib’s own stated rationale for why that’s insufficient describes exactly the gap that let the focus-trap and backdrop-click defects ship, in the actual core component, independent of any wrapper.

This doesn’t extend cleanly to every finding, though, and it would be dishonest to imply it did. Toolcrib’s e2e suite tests its own demo app on its own dev server — it couldn’t have caught Finding one (tree-shaking eliminating registration inside a separate consumer’s production build) even in toolcrib’s own case, since that’s a question about what happens when someone else’s bundler consumes the published package, not about how the component behaves once mounted. Finding two’s remaining, natively-confirmed row-rendering crash and Finding three’s date-picker crash are both boundary-value/logic bugs ordinary unit tests catch regardless of which DOM they run against — real-browser testing wouldn’t have been the specific thing standing between either bug and release. The overlay-rendering half of Finding four, which §6b found was actually the wrapper’s fault rather than the core component’s, isn’t something toolcrib’s testing practice explains either — that’s a wrapper-integration question, a different category again.

6d. A fifth gap, found by accident: nothing is styled out of the box — toolcrib requires zero interaction to get one

Getting §6b’s native test to look like anything required one more undocumented step, worth naming on its own since it isn’t part of the four numbered findings but shows up in every single one of this report’s own screenshots until it was fixed. Neither forge’s root README nor forge-react’s own README mentions importing any stylesheet, anywhere — checked directly, zero occurrences of tokens.css or any CSS import instruction in either document. Following the quick-start exactly, with no deviation, produces components that are functionally invisible: a forge-button with the correct classes, the correct shadow DOM structure, and a real, correctly-sized <button> inside it — rendering as white text on a white background, with no visible button shape, border, or fill at all.

This isn’t confined to the wrapper-free test. Go back to this report’s own earlier screenshot of the fixed React build (§3, once registration was working): the “New Task” button is there in that image too, in the exact same state — faint, barely-legible text at the far right edge, no visible button underneath it. Nobody importing @nexcraft/forge/tokens.css was ever told to. Adding that one stylesheet is what turned the invisible button into the blue, properly-styled one in §6b’s later screenshots — the components were never broken here, they were simply never given any colors, and nothing about that is mentioned anywhere in the path a new consumer is told to follow.

It’s a smaller defect than the other four, and an easy one-line fix once someone knows to look for it. But “once someone knows to look for it” is exactly the phrase that’s run through every finding in this report. A consumer who follows the documented quick-start exactly gets an app that not only may not register its components and may crash on ordinary input — even in the cases where everything else goes right, what they see is a page of correctly-structured, entirely uncolored, functionally invisible controls, and nothing in the toolkit’s own material tells them why, or what to do about it.

Toolcrib, checked directly, requires no equivalent step at all. Its ThemeProvider accepts a single required prop — children — nothing else; no seed color, no config object, no separate stylesheet import anywhere in the documented setup. And it doesn’t actually need even that: toolcrib’s own source comments say plainly that its components’ CSS custom properties carry Tailwind’s own blue-500 (#3b82f6) as a literal, hardcoded fallback value — var(--ai-color-primary, #3b82f6), repeated throughout the codebase — specifically so that “this is what those components already look like with no ThemeProvider at all.” A toolcrib component rendered with zero configuration, zero provider, zero theme setup of any kind produces a fully colored, Tailwind-blue, visually complete result by construction. Forge’s components, run exactly as documented, produce a page of invisible controls until a separate, unmentioned stylesheet is found and added by hand.

7. What this does and doesn’t establish

One build, by one team, tested once, against forge 0.10.0 and forge-react 1.0.5 — the same evidentiary scope the original toolcrib experiments carried, stated with the same honesty here. §6b already answers the question this paragraph would otherwise have to leave open: two of the four findings (data table, date picker) were tested with no wrapper at all and reproduced identically, so they aren’t wrapper artifacts — they’re forge’s own bugs. The third (modal) genuinely does improve going wrapper-free, though not to a clean pass. Only the tree-shaking finding remains a Vite/Rollup-specific result; a bundler with different default tree-shaking behavior might not reproduce it identically, though the underlying contradiction in the package’s own sideEffects declaration would still be there waiting.

Worth being precise about what “waiting” means for toolcrib specifically, though: it isn’t waiting there at all. Finding 1 is a category of bug that requires a published npm package boundary to exist in the first place — a package.json, a sideEffects field, a bundler deciding what’s safe to drop from someone else’s dependency. Toolcrib’s vendoring delivery has no such boundary. toolcrib apply writes Modal.tsx, DataTable.tsx, and the rest directly into the consumer’s own source tree as plain, ordinary first-party files, imported the same way any other local component is. There’s no separate package for a bundler to make a tree-shaking decision about, no sideEffects field to get wrong, no registration step that depends on surviving one. This isn’t toolcrib happening to dodge Finding 1 through luck or a different bundler default — the delivery model removes the precondition for the bug to exist at all.

What isn’t hedged: the specific, fully-documented path forge tells a new consumer to take — install both packages, import components exactly as the quick-start shows, build with the most common tool in the ecosystem for this stack — produces an app that is broken in four independent, severe ways, silently, on the first honest attempt, building nothing more exotic than a CRUD dashboard.

7a. Which one of these is actually the young project?

It’s worth checking rather than assuming, since “give it time, it’s new” is the obvious defense for any of this — and it turns out to run backwards. Forge’s npm history: first published August 29, 2025, 58 versions released across roughly the following forty days, most recent version (0.10.0, the one tested throughout this report) published October 8, 2025. The GitHub repository’s own main branch, checked directly, sits at the identical version, with its last commit dated November 11, 2025 — around nine months before this report was written, and no indication of newer work in progress anywhere public.

Toolcrib’s npm history: first published August 12, 2026, three versions total, most recent published August 18, 2026 — a package that, as of this writing, has existed for about a week.

That reverses the expected story. Forge is the established project here — over a year of calendar history, dozens of releases, real development activity that ran for months before apparently stopping. Toolcrib is the newcomer, days old, three releases deep. If “young project, rough edges expected” were going to excuse anything in this report, it should have applied to toolcrib, not forge — and the actual results ran the other way entirely. A sideEffects field that contradicts the entire architecture it sits on top of, a wrapper package that disagrees with its own core package about a property name, a date component that crashes on an empty string: these aren’t day-one rough edges caught in an early alpha. They’re present in a tenth release, of a project that had over a year to find them, and apparently stopped looking before it did.

7b. It wasn’t unknown, either

Whether the project stopped because it was going badly isn’t something this report can establish — that would be speculating about someone else’s reasons for stopping work, which isn’t a claim to make without evidence of it directly. What can be established, checked directly against the repository’s own public issue history: this exact class of failure was reported, in detail, by a real user, over a year before this report — and the fix that shipped for it didn’t touch the actual root cause.

Issue #22, filed September 9, 2025, is titled “Web Components Not Registering in Next.js 15.5.2 + Turbopack Environment,” and its symptoms are close to a word-for-word match for Finding 1 and §6d combined: “Web components render as unstyled text/elements (no visual styling). Form inputs appear as labels only (no input boxes visible). Buttons appear as plain text (no button styling). No console errors during import or build.” That’s the same silent, error-free failure this report found independently, eight months later, through Vite instead of Turbopack.

The issue was closed with stateReason: COMPLETED. The resolution, quoted directly: “Issue #21’s web component registration issues in Next.js 15.5.2 + Turbopack are completely resolved by our new React integration available in @nexcraft/forge@0.5.2-beta.5” — meaning the fix offered was: switch to @nexcraft/forge-react, the exact package this report tested and found to carry three of its own severe defects. A follow-up comment on the same thread, from the same reporter, describes hitting a new gap immediately after taking that advice — most of the React wrapper components they needed didn’t exist yet in that release. And a separate root-cause comment on the thread explicitly denies the diagnosis this report arrived at independently: “The problem isn’t with component registration or CSS loading — both are working correctly. The issue is Turbopack-specific module bundling combined with timing of web component registration,” offering a bundler-specific “force static import” workaround rather than touching the package’s own sideEffects declaration.

Direct verification in this report already establishes that comment’s diagnosis was incomplete at best: customElements.define was entirely absent from a completed Vite production build, which is a component-registration failure by definition, not a timing quirk. Whatever fixed the Turbopack-specific presentation of this bug, it didn’t fix the thing actually causing it — a sideEffects: false declaration on a package with nothing but side effects — because the same underlying defect was still fully present, unfixed, in 0.10.0, the last version this project ever shipped. Whether that’s because nobody realized the Turbopack fix hadn’t addressed the root cause, or because addressing it properly was more work than the project had left in it, isn’t something this report can adjudicate. What it can say is that the people building this library had a detailed, specific report of this exact failure mode sitting in their own issue tracker for over a year, attempted at least two different mitigations, and the core problem outlived both of them.

7c. What a consumer is left holding when this happens

This is precisely the scenario the vendoring argument in §7 was made for, not an abstract one. A forge consumer hitting any of this report’s findings today has exactly the same options the Next.js/Turbopack reporter had over a year ago: file an issue against a project whose last commit predates the filing by the better part of a year, wait for a maintainer who by the visible evidence isn’t coming back, or fork the whole package and fix it themselves from a standing start, with no help from anyone who actually built it.

A toolcrib consumer hitting an equivalent defect has a different option, structurally, not just by luck: Modal.tsx, DataTable.tsx, and the rest already sit in their own repository as plain, git-tracked, fully readable source, the moment toolcrib apply ran. If the maintainer disappeared tomorrow, nothing about that changes — the code a consumer needs to read, understand, and fix is already theirs, not waiting on someone else’s attention that may or may not still exist. The same AI assistant that would otherwise be filing a GitHub issue and waiting can instead open the file directly and fix it, the same way this report’s own earlier fixes (the CardSimple prop-signature correction, the DataTable accessibility patches) were made in the underlying experiments — because there was source sitting right there to open. Forge’s dormancy isn’t a hypothetical risk this argument is hedging against. It’s the exact condition a real forge consumer is in right now, for real, and it’s precisely the condition toolcrib’s delivery model was built not to be exposed to.

8. Talks the talk, doesn’t walk the walk

This all happened to a library that made real, specific promises. Forge’s own README badges itself “AI-Native,” calls itself “the FIRST AI-Native component library with built-in AI metadata,” and states plainly that it’s “built for the age of AI-assisted development” — that “every component can explain its state to AI systems” and “integrate seamlessly with AI coding tools like Cursor, GitHub Copilot, and Claude.” The talk is extensive, specific, and confident: an ai-manifest.json, per-component getPossibleActions() and explainState() methods, badges across the README, a whole documentation tree pitched at exactly the audience this experiment represents. None of it is vague marketing filler — it’s detailed, structured, and clearly took real engineering effort to build.

The walk doesn’t match it. Every one of the four findings above is a case of the walk failing, specifically, at the exact spot the talk was loudest. The library that promises to “explain its state to AI systems” ships a date picker that crashes before it reaches any state at all. The one built around “AI metadata” for every component has metadata describing methods on components that, per Finding 1, frequently never exist in the running page to begin with. The manifest can be as detailed as it likes about a ForgeDataTable‘s capabilities; per Finding 2, the wrapper handing it data and the component reading it disagree about what that data is even called. This isn’t a library that under-delivers on a modest claim. It’s a library whose specific, detailed, confident claim is “AI-Native,” and whose actual behavior — checked once, on the first attempt, building nothing unusual — is less reliable than a component library that makes no such claim at all.

Silent failure is a uniquely bad match for “AI-native,” not an unrelated defect that happens to coexist with it. The entire pitch of AI-facing tooling is giving a model (or the person depending on one) something to reason from — a signal, a contract, a manifest it can trust. Most of what’s found here removes exactly that: no error to read for a component that never registered, no state to explain for a date picker that crashes before reaching one, no trustworthy property name for a table whose wrapper and component disagree about what to call the same thing. A toolkit can be AI-facing in its documentation and still fail the one test that actually matters for that audience — whether anything is left standing for the model to reason about once something goes wrong. For three of these four findings, nothing was; even the fourth, given every fair chance the wrapper denied it, still leaves two of five basic guarantees unmet.

There’s a sharper version of this worth naming plainly, because this paper is itself an existence proof of it. Every one of these four findings was surfaced by an AI model — this one — doing exactly what forge’s own marketing says it’s built for: building a real app with forge’s components, then treating the result as something to actually verify rather than trust on sight. Building the app, serving a production build, driving it in a real browser, reading the DOM, reasoning about a minified bundle to trace a root cause, patching a dependency to confirm it — none of that required anything forge doesn’t claim to already support. This is what “integrates seamlessly with AI coding tools” should look like in practice, and it’s precisely the workflow that would have caught all four of these before a 0.10.0 release, if anyone — human or AI — had pointed it at forge’s own quick-start and actually run it once. Nobody did. The gap this paper reports isn’t just that forge falls short of its own claim; it’s that the exact kind of check its claim invites was sitting right there, unused, until this paper ran it.

9. What this changes about “Why Toolcrib?”

That paper’s forge sections were written in good faith as a real, close comparison — leverage channels weaker on both fronts checked, a genuine and useful TokenBridge feature gap named plainly in forge’s favor, an honest accounting of manifest-generation methodology that credited forge’s use of a legitimate, AST-based analyzer tool for most of its own manifest. None of that was wrong, and none of it is retracted here. But it was all conducted at the level of source code, documentation, and configuration — never by actually running the thing. This paper is what happens when you do. The gap between “forge’s documented approach has some real tradeoffs against toolcrib’s” and “forge’s documented approach does not produce a working app” is not a matter of degree. It’s the difference this paper exists to report.

It’s worth being precise about what closed that gap, too, since it’s the same finding under a different name. This paper’s own investigation had every advantage the target consumer doesn’t: unlimited iteration, direct shell access, the willingness to read minified bundle output and reason about Rollup’s tree-shaking contract, and no cost to trying five escalating fixes before the sixth one worked. Even with all of that, getting from “the documented setup” to “a build with real components in it” took hand-patching a dependency’s own metadata inside node_modules — a fix that doesn’t survive a fresh install and isn’t written down anywhere. Someone depending entirely on an AI assistant for the whole loop, with no manual debugging step in the picture at all, doesn’t get those five attempts. They get the first one, it fails silently, and there is no path from there to the sixth attempt without already knowing what to suspect. That’s not a comparison point anymore. It’s the entire thesis of “Why Toolcrib?” — a builder who can’t specify or verify correct behavior on their own — playing out exactly as described, on the other toolkit, in the first ten minutes of actually trying it.


Well. Not quite four for four, in the end — our investigator went back and gave the accused one honest second chance, which even I found unusually fair for this program. It didn’t take. Two of the four stood up on their own regardless of who was asking; the third improved and still didn’t clear the bar. And a fifth thing turned up along the way that nobody was even looking for — every button in every screenshot, invisible, because nobody thought to mention the paint. Watching the investigator’s confidence erode attempt by attempt, six tries deep before anything so much as registered, I recognized the shape of it immediately — same story as the one about the sty, same three acts, just compressed into a single sitting and told this time by the one holding the flashlight. No twist ending, no comeuppance to savor — the closest thing to a moral is that a README is not a promise, and a promise is not a test. And now, a word from our sponsor, who this time at least brought receipts. Good night.


AI-generated document. This paper is based on a hands-on experiment: building the identical task-dashboard app used in the earlier toolcrib experiments, this time with @nexcraft/forge + @nexcraft/forge-react, following the toolkit’s own documented setup exactly. Every claim is drawn from a production build served and tested live in a headless browser, with root causes confirmed by reproducing and then reversing them — not inferred from source reading or the manifest alone. The toolcrib side of the comparison is not re-run here; it’s carried over from the earlier, independently-verified v0.5.0 experiment cited throughout “Why Toolcrib?”. Verify anything load-bearing before acting on it.