Refactor Application Experiment: Uncontrolled vs. Toolcrib

This report, the code it describes, and the patches accompanying it were generated by an AI model (Claude). Every claim here was checked against a running app, a real repo checkout, or a live browser session rather than left as an assertion — the methodology and exact commands are in the Appendix specifically so any of it can be independently re-run. But “extensively verified” isn’t the same as “immune to the failure modes this report itself describes,” and that applies with particular force to the parts proposing fixes for someone else’s repository (ACTION_PLAN.md, the verbatimModuleSyntax patch): treat them as a diagnosed starting point for a human or another AI session to review, not as pre-approved changes to merge unread.

Factors: uncontrolled (free-form AI-generated React/Tailwind) vs. toolcrib (escape-llc/toolcrib CLI refactor) Measurement: how much of the uncontrolled markup gets refactored into toolcrib’s components — and, beyond raw coverage, whether the two implementations actually behave differently where it matters.

This report covers three kinds of comparison:

  • Markup coverage (Method → Bottom line): how much hand-rolled JSX got replaced with toolcrib components, and what didn’t convert.
  • Head-to-head behavioral comparisons, each tested live in a browser rather than inferred from source or the component manifest:
    • Modal — dialog semantics, focus trap, Escape/backdrop dismiss, label association (“Correctness & quality: beyond markup coverage”)
    • Table — sorting, pagination, virtualization vs. native semantics, keyboard access (“DataTable: head-to-head on features and correctness”)
    • Form — validation behavior, error display, and what’s (and isn’t) exposed to assistive tech in each (“Form validation: head-to-head”)
  • Discussion: the repair loop — four real gaps surfaced in toolcrib itself (listed in full, with fixes, in the companion ACTION_PLAN.md). Rather than treat that as a strike against the tool, this section argues the gaps matter less than they would in the uncontrolled leg, because toolcrib has a mechanism (doctor/merge) for a fix found once to propagate back to every consumer, where hand-rolled code has no equivalent path at all — and examines why the gaps cluster specifically in toolcrib’s own from-scratch code rather than the parts wrapping an already-hardened library, plus what that implies about AI-generated tests as a check on AI-generated code.

Method

  1. Scaffolded a Vite + React 19 + TypeScript + Tailwind project (uncontrolled).
  2. Built a task/project dashboard app: header, 4 stat cards, a toolbar (search + 2 filter selects + new-task button), a sortable-looking task table with edit/delete actions, and a full modal form (title, description, assignee, due date, priority, status). All markup hand-rolled JSX with Tailwind classNames — no shared component library.
  3. Counted the baseline: 87 raw HTML elements, 65 className attributes across App.tsx + 5 component files.
  4. Forked the project to toolcrib_leg. Ran:npx toolcrib init --situation refactor --version 0.4.0 Reviewed all 116 staged patches in ./toolcrib-patches/, then npx toolcrib apply. Installed the added deps (radix-ui, zod). Confirmed the project still built with toolcrib vendored but not yet used.
  5. Read toolcrib/ai-docs/CORE.md, REFACTOR_APP.md, and component-manifest.json (the files toolcrib init vendors specifically to brief an AI agent on the toolkit).
  6. Refactored every file against the toolcrib API:
    • main.tsx — added required <ThemeProvider> + <ToastProvider> + <ToastContainer> root wrapper
    • App.tsx — raw <header>/<main> → <AppShell> + <VStack>
    • StatCards.tsx — raw grid of <div>s → <Grid> + <CardSimple>
    • Toolbar.tsx — raw <input>/<select>/<button> → <Toolbar> + <Input> + <Select> + <Button>
    • TaskTable.tsx — raw <table> → <DataTable> with column definitions
    • TaskModal.tsx — raw <form> + manual useState draft object → <Modal> + <Form schema={zodSchema}> + <FormField> wrapping <Input>/<Select>/<Textarea>
  7. Re-ran the same element/attribute counts on the refactored code.
  8. Verified functional equivalence in a real browser (Puppeteer): status filter, edit-modal prefill, and delete all confirmed working identically to the original.

Results

MetricUncontrolled (before)Toolcrib (after)Change
Raw HTML elements878−90.8%
className attributes652−97%
Toolcrib component tags used041—

What’s left raw, and why (all legitimate, not refactor failures):

  • <h1>/<p> in the app header — plain text content, nothing to replace.
  • 2 <span>s in Badge.tsx — toolcrib has no badge/pill/tag component in its manifest (confirmed by searching it directly). A genuine coverage gap, not a missed conversion.
  • 4 small <div>s used as text-wrapping containers inside table/stat-card cell renderers — below the granularity toolcrib operates at.

Functional verification

Screenshots alone don’t prove equivalence, so the refactored app was driven in a headless browser:

  • Status filter — selecting “Done” correctly narrowed the table from 6 rows to the 2 actual “Done” tasks; stat cards stayed accurate.
  • Edit prefill — clicking “Edit” on “Write Q3 retro notes” opened the modal with every field correctly populated (title, description, assignee, due date, priority, status) via <Form initialValues> — replacing what was previously a hand-written useEffect/useState sync in the uncontrolled version.
  • No console or page errors during any interaction.

Friction points encountered

  1. api.github.com rate limit. Unauthenticated CLI calls (toolcrib versions, init without --version) hit a 403 on this sandbox’s shared egress IP. Worked around by pinning --version 0.4.0 directly — the download itself uses plain github.com release-asset URLs, not the rate-limited API.
  2. verbatimModuleSyntax incompatibility. Vite’s default React+TS template enables this strict TypeScript option; toolcrib’s vendored source uses plain (non-import type) type imports throughout, which fails the build under it. toolcrib doctor checks moduleResolution compatibility but not this specific option — had to manually disable it in tsconfig.app.json. This would block any real adopter’s first build.
  3. CardSimple requires children — its title/subtitle props are header-only, not a full replacement for a card’s main content. A one-line fix once checked against the actual component source rather than assumed from the manifest’s short description.

Correctness & quality: beyond markup coverage

Element/attribute counts show how much markup moved, but say nothing about whether the result actually behaves better. Toolcrib’s stated premise is that using its components buys you correctness guarantees (accessible dialog semantics, focus management, keyboard support) “for free” — so this was tested directly against the running apps, not assumed.

Method: scripted the same interaction sequence (open the “New task” modal, inspect DOM, tab through the form 30 times, press Escape, click the backdrop) against both apps in a headless browser, plus captured console output throughout.

CheckUncontrolledToolcrib
Modal has role="dialog"❌ plain <div>✅
Modal has a wired accessible name (aria-labelledby)❌ none✅ (Radix auto-wires to the header text)
Label ↔ input association (for/id resolves)❌ 0 / 6⚠️ 3 / 6
Focus trap (Tab can’t reach the page behind the modal)❌ escapes after a few tabs✅ trapped
Escape closes the modal❌ no-op✅
Clicking the backdrop closes the modal❌ no-op✅
Console errors/warnings during usenonenone

Uncontrolled app — confirmed against source, not just observed behavior: the backdrop <div> has no onClick at all, and there’s no keydown/Escape listener anywhere in the file — only the explicit “✕” and “Cancel” buttons close it. None of the six <label> elements have a for attribute, and none of the inputs have a matching id, so a screen reader cannot associate any label with its control. This is exactly the kind of defect a human reviewer skimming the rendered UI (rather than testing it) would miss — it looks correct and reads correct, it just isn’t wired.

Toolcrib app — a real, partial win, not a clean sweep: <Modal> (built on Radix’s Dialog primitive) gets dialog semantics, a full focus trap, Escape-to-close, and backdrop-dismiss entirely for free — none of that was written by hand, all of it verified working. But label association only holds for 3 of 6 fields: <Input>/<Textarea> correctly forward id={name} to match <FormField>‘s <Label htmlFor={name}>, while <Select> does not — its SelectProps interface doesn’t even expose an id prop, and its underlying SelectPrimitive.Trigger renders with no id and no aria-label, confirmed directly in the DOM. So “Assignee,” “Priority,” and “Status” (and, by the same code path, both toolbar filter selects) have no accessible name at all for a screen reader — worse than an unassociated label, since there’s no fallback either. This is a genuine bug in toolcrib’s own <Select> component, not a refactor mistake on this project’s part, and would be worth its own upstream patch if useful (same shape as the verbatimModuleSyntax fix: a small, mechanical, verifiable change — add id={fieldName} to the Trigger).

Bottom line on this axis: the premise holds directionally — adopting toolcrib’s overlay and form primitives did eliminate several real, verified interaction/accessibility bugs that existed in the hand-written version, without anyone writing focus-trap or keyboard-dismiss logic by hand. But it’s not an unconditional guarantee: one of toolcrib’s own components has a real accessibility gap that partially undermines the very pattern (FormField + Label htmlFor) the toolkit is designed around.

DataTable: head-to-head on features and correctness

Same treatment as the modal — tested actual behavior in-browser, checked source for root causes, didn’t just compare markup.

Uncontrolled table: a real <table>/<thead>/<tbody>/<th> with correct native semantics, but genuinely static — no sorting, no pagination, <th> elements are plain headers with no interactivity at all. Confirmed directly in source: zero onClick/sort state anywhere in the file.

Toolcrib <DataTable>: implements virtualization without abandoning table semantics — worth calling out as a good design choice, since many virtualized-table implementations fall back to <div> grids and lose native table accessibility entirely. This one still renders real <table>/<thead>/<tbody>/<th>/<tr>/<td>, with virtualization done via spacer <tr>s above/below the visible window — confirmed in source.

CheckUncontrolledToolcrib
Sorting❌ none✅ works, verified correct (asc/desc toggle, correct reordering, ▲/▼ indicator)
Pagination❌ none (renders all rows)✅ present, correctly disabled/hidden logic when all rows fit on one page
Native <table> semantics preserved under virtualizationn/a (not virtualized)✅ yes — spacer-row approach, not a div-grid
Sortable headers reachable by keyboardn/a❌ tabIndex="-1" on every <th>, no role, no keydown handler — mouse-only
aria-sort reflects current sort staten/a❌ not set on any header, in either sort state
sortable default matches its own documented contractn/a❌ mismatch — see below

Confirmed bug: sortable‘s default contradicts its own JSDoc. The Column.sortable prop is documented as @default false (“if true, clicking toggles sorting”). The actual click handler checks col.sortable !== false — so a column that simply omits sortable (the documented default, not-opted-in state) evaluates undefined !== false → true and becomes sortable anyway. In this project’s own TaskTable.tsx, the “Actions” column never sets sortable and isn’t sortable data — but the bug makes its header render with cursor: pointer (a false affordance) and become clickable. Confirmed no crash results (the sort comparator finds undefined on both sides and no-ops), but it’s a real, user-facing inconsistency between what the docs promise and what the code does — same “verify, don’t just read the manifest” lesson as the CardSimple/children prop earlier in this report, just with a more concrete downstream effect.

Not observed but worth flagging as a latent risk, not a confirmed bug: virtualization positions off-screen rows using a fixed itemHeight (default 44px) for the spacer-row math, while this project’s “Task” column renders two lines of custom content (title + description) at ~39px measured height — close enough that it didn’t clip with 6 rows, but a taller custom cell renderer at real pagination scale (many rows, itemHeight not explicitly overridden) could plausibly drift from the actual rendered row height. Didn’t reproduce this at the scale tested; flagging as a thing to watch, not a demonstrated defect.

Bottom line on the table: toolcrib’s DataTable is a genuine upgrade in capability — free sorting and pagination that didn’t exist before, implemented without sacrificing table semantics under virtualization, which is a real engineering credit. But it inherits the same pattern as the Select finding above: real functionality, real gap. The sortable headers are entirely unreachable by keyboard, and the sortable prop’s own documented default doesn’t match its implementation — two more concrete, verified findings, not speculation, worth their own upstream fixes if useful.

Form validation: head-to-head

Same standard again — tested actual behavior, checked source, didn’t just compare the schema declaration to the JSX.

Uncontrolled form: only title has any validation at all — a native HTML5 required attribute. Confirmed in source: no other field (description, assignee, dueDate, priority, status) has any constraint whatsoever. Tested the one constraint that exists: submitting with an empty title is correctly blocked by the browser’s native Constraint Validation API — input.validity.valid is false, :invalid matches, and the browser shows its own native tooltip (“Please fill out this field.”). This works, but it’s a single hand-placed attribute; nothing else in the form is validated at all.

Toolcrib form: the Zod schema’s title: z.string().min(1, 'Title is required') was tested the same way — submit immediately with an empty title, no prior interaction with any field. Confirmed:

  • Submission is blocked (modal stays open)
  • The custom message “Title is required” renders inline, styled in red, directly below the field — on the very first submit attempt, without requiring a prior blur first (the Form‘s submit handler marks every field touched before validating, so a never-touched field still surfaces its error immediately rather than waiting for the user to tab through it)
  • Fixing the title and resubmitting closes the modal and adds the new row correctly — confirmed end-to-end, not just that the error clears

A gap common to both, worth being even-handed about: neither implementation wires aria-invalid or aria-describedby to connect the error state to the input programmatically. Checked both directly in the DOM post-submit-attempt: uncontrolled’s native-invalid input has aria-invalid: null (browsers don’t add this automatically just because :invalid matches); toolcrib’s <Input> in error state also has aria-invalid: null and aria-describedby: null despite rendering a visible, correctly worded error message right next to it. So a screen reader user gets something from the native path (browser-level exposure of the constraint-validation tooltip, inconsistent across browsers) and gets silence from toolcrib’s path (a sighted-only red-text error) — neither is a clean win here, and toolcrib’s FormField/Input pairing would need aria-invalid={!!error} and aria-describedby pointing at the error <span>‘s id to close this gap. Small, mechanical, same shape as every other fix in this report.

Where the schema approach is a genuine, structural improvement, independent of the gap above: a single Zod schema is one source of truth that every field binds to automatically, versus native HTML5 validation requiring a hand-placed attribute per field with a fairly limited vocabulary (required, pattern, min/max, no cross-field rules, no custom messaging without extra JS). Worth being honest that this project’s own schema didn’t fully exploit that ceiling — assignee is typed z.string() rather than z.enum(assignees), and dueDate has no non-empty check, so both apps actually accept a task with no due date. That’s a modeling choice made while writing this experiment’s schema, not a toolcrib limitation — the toolkit would enforce any of that just as easily if the schema said so.

Bottom line on forms: toolcrib’s validation is more capable and more consistently applied across the whole form than the uncontrolled version’s single required attribute, and it demonstrably works correctly end-to-end. But “adopting a schema-driven form” didn’t automatically deliver full accessibility for free — the error-to-input wiring is a real, separate gap, present in toolcrib’s own component, not something this project’s usage pattern could have fixed by writing a better schema.

Bottom line

For a UI with strong overlap with toolcrib’s supplied component categories (overlays, tables, forms, layout primitives), the refactor achieved ~91% raw-markup elimination and ~97% styling-attribute elimination in a single incremental pass, guided entirely by the vendored ai-docs/ + component-manifest.json. The only markup that couldn’t convert was content genuinely outside the toolkit’s scope (plain text) or a missing primitive (badges). Two real, non-trivial friction points surfaced along the way (API rate limiting during init, and a TypeScript strictness incompatibility) that a first-time adopter would hit before ever reaching the refactor step itself.

Markup coverage was only half the question, though. Testing actual behavior — not just counting tags — showed the refactor also fixed several genuine, verified bugs (no dialog semantics, no focus trap, dead Escape/backdrop handlers, zero label association) that existed in the hand-rolled version, and added real capability that never existed before (sorting, pagination) without sacrificing native table semantics. That’s toolcrib’s stated value proposition actually paying off, empirically, not just asserted.

It’s not unconditional, though, and a rigorous eval should say so rather than round up. Four concrete, verified gaps surfaced in toolcrib itself: <Select>‘s trigger has no id/aria-label at all (breaking label association for exactly the fields that use it), DataTable‘s sortable headers are mouse-only with no keyboard path and no aria-sort, Column.sortable‘s actual behavior contradicts its own documented default, and Input‘s error state isn’t wired to aria-invalid/aria-describedby despite FormField rendering a clear, correctly worded error message right next to it. All four are small, mechanical, and independently verifiable — the same shape as the verbatimModuleSyntax fix already handed off — but they’re real, and “adopting toolcrib” measurably improved correctness here without making it perfect.

Discussion: the repair loop

Why these gaps matter less than they would in the uncontrolled leg

Every gap found in this report — the missing Select id, the unwired aria-invalid, the keyboard-inaccessible sort, the sortable default bug — has a fix available two ways right now: verbatim-module-syntax-fix-HEAD-full.patch and ACTION_PLAN.md, both handed off toward the actual escape-llc/toolcrib repo. That handoff is only possible, and only useful at scale, because of a mechanism the uncontrolled leg has no equivalent of at all.

Uncontrolled: the four modal/table/form bugs found here live nowhere but this one project’s TaskModal.tsx/TaskTable.tsx/Toolbar.tsx. There is no upstream for them. Fixing them means editing these exact files, in this exact project, by hand. If another project independently hand-rolled a similar modal — which, structurally, most AI-assisted “vibe coded” apps do, since that’s the whole premise toolcrib‘s own README states — it has the same class of bugs sitting in its own copy, undiscovered, unconnected to this report, and would need the same debugging effort spent again from zero. Nothing about finding and fixing it here helps anyone else.

Toolcrib: the CLI’s init → apply → doctor → merge lifecycle exists specifically to prevent that duplication. merge compares three things per vendored file — what a project originally got, what’s actually on disk now, and what a new release ships — and sorts each into “safe update” (never hand-touched locally; the new version is staged as a patch), “kept as-is” (locally edited, and upstream didn’t change that file — left alone), or “conflict” (both changed; a .upstream-diff is written explaining what upstream did, so a person or their AI reconciles it deliberately rather than one side silently winning).

Concretely, in this experiment: toolcrib_leg never hand-edited Select.tsx, DataTable.tsx, or FormComponents.tsx — every use of those components went through the public API, not a local patch to the vendored source. That means if escape-llc/toolcrib accepts the fixes in ACTION_PLAN.md and ships them in a new release, running toolcrib merge in this exact project would detect all four files as clean “safe updates,” stage them as reviewable patches, and toolcrib apply would close all four gaps — without anyone here re-diagnosing them, re-writing the fixes, or even remembering this report existed. The fix travels from wherever it was first found (here) to every consumer that adopted the toolkit, mechanically, with the tooling itself protecting any local customization along the way. The uncontrolled leg’s equivalent bugs have no such path: they only leave this project if a person manually notices this report, manually applies the same reasoning to different files, and manually repeats the process per project, per bug, indefinitely.

That asymmetry — not the markup-conversion percentage this report opened with — is arguably toolcrib‘s strongest claim on “quality of operations”: not that the vendored code starts out bug-free (it demonstrably doesn’t), but that a bug found once has a mechanical, conflict-aware path back to every project that adopted it, which hand-rolled code structurally cannot offer no matter how carefully it’s written.

Where the gaps cluster, and why

The four gaps found in this report aren’t evenly distributed across toolcrib’s components, and the pattern is worth naming explicitly rather than leaving as a footnote.

Nobody prompting “build a modal” — whichever side of this experiment is doing the prompting — carries aria-modal, focus traps, or aria-invalid as part of what a modal is. That knowledge isn’t common even among engineers who’d never touch an AI tool; it’s specialist accessibility knowledge, not general withheld competence. So there’s no reason to expect that requirement to surface in generated output by default, on either side of this experiment, unless something other than the prompt supplies it.

That “something else” is exactly what splits toolcrib’s own results cleanly in two:

  • <Modal> is a thin skin over Radix’s Dialog primitive. Every check passed: role, focus trap, Escape, backdrop-dismiss — all correct, all unprompted. That’s not because toolcrib’s construction carried the accessibility signal; it’s because Radix’s own maintainers do, as their entire reason for existing, and toolcrib inherited their already-solved output wholesale.
  • <Select> also wraps a Radix primitive — but the specific thing that broke (id never threading from toolcrib’s own fieldName down to SelectPrimitive.Trigger) isn’t Radix’s problem to solve; Radix has no idea toolcrib’s FormField pattern exists. That connective code is original, toolcrib-authored glue, and it’s exactly where the gap is.
  • <Input>‘s missing aria-invalid/aria-describedby and <DataTable>‘s three separate issues (keyboard-inaccessible sort, missing aria-sort, the sortable default bug) aren’t wrapping anything. DataTable in particular has no Radix underneath it at all — and it’s also where defects are most concentrated: three distinct, independent bugs in one from-scratch component.

Every gap found in this report sits in code toolcrib’s own authors wrote from scratch. Every check that passed sits in code borrowed from a library whose maintainers had the missing signal already built in. Given toolcrib’s own README states plainly that “the rest was generated by AI, using various IDEs and AI models,” this isn’t a coincidence of small sample size — it’s the same mechanism the rest of this report describes, visible one layer down. Toolcrib doesn’t transcend the signal problem so much as launder it: it succeeds precisely where it borrowed someone else’s already-solved instance of correctness, and reproduces the identical gap everywhere it had to solve the problem fresh — because “build a modal” and “build a sortable table” carry exactly the same missing signal regardless of who, or what, receives the instruction.

This is a falsifiable claim, not just a narrative fit to the four bugs already found: it predicts the next defect in this codebase is more likely sitting in DataTable, Toolbar, or another from-scratch component than in anything built directly on a Radix primitive. Worth checking against whatever escape-llc/toolcrib fixes or doesn’t fix next.

Why the floor matters more than the bug count, and why AI-written tests don’t fully fix that

Toolcrib’s own stated philosophy isn’t “these components are bug-free” — it’s closer to giving vibe-coding practitioners a better floor to build from, on the premise that regression-testing an entire UI after every single turn is an unreasonable ask of anyone, and that even a project with unit and e2e tests is only getting one level of assurance, especially when those tests are themselves AI-generated. Both halves of that are worth taking seriously on their own, not just as a defense of the four gaps found here.

The floor, not the bug count. The value of <Modal> was never that this instance happens to be correct — it’s that “does this modal have a focus trap” stops being a question re-asked, and re-answered from scratch, on every single turn that requests a modal, in every project, forever. In the uncontrolled world, each occurrence independently re-samples the same latent distribution — whether this modal gets a focus trap is drawn fresh every time, with no memory of the last hundred times it wasn’t. Once a component exists and a bug in it is fixed centrally — via exactly the merge path described in the Epilog — that fix is permanently subtracted from the risk surface for every future turn that calls <Modal> instead of regenerating one. The floor rises not because the generation got smarter, but because “regenerate this from scratch” is being asked for less and less often.

AI-generated tests confirm the generated code, not some objective truth. If the same model (or the same class of model, drawing on the same latent space) writes both the implementation and the test suite for it, the test isn’t an independent check — it’s a second draw from the same distribution the implementation came from. A missing signal that produced <Select>‘s absent id or <Input>‘s absent aria-invalid is exactly the kind of gap a self-authored test is also unlikely to catch, for the same reason: nothing in “build a modal” surfaces aria-modal as a requirement to implement or to test for. A green suite in that situation confirms the code matches its author’s mental model: it says nothing about whether that mental model was complete.

This is, incidentally, a fair description of why this report’s own checks held up: none of them came from a test some model wrote for itself. They came from an external, pre-existing standard the generation loop had no hand in — the WAI-ARIA authoring practices for dialogs and sortable tables specify role, aria-sort, focus containment, keyboard reachability as known, external requirements, checked against the live DOM directly, not against self-referential test expectations. It’s the same underlying reason toolcrib doctor‘s drift detection and merge‘s conflict flagging are worth more than a green test run alone: the check’s value comes precisely from sitting outside the generation it’s checking, the same way the fixes in ACTION_PLAN.md came from driving a real browser against a real standard, not from asking the code whether it thought it was correct.

Appendix: tooling used for verification

Every claim in this report was checked against a running app or a real repo checkout, not inferred from source reading alone. Precise tooling, for reproducibility:

Browser automation — Puppeteer, not Playwright. Headless Chrome (a locally available “Chrome for Testing” build) driven via puppeteer (already present in this environment through a global @mermaid-js/mermaid-cli install), launched with --no-sandbox --disable-setuid-sandbox. Both apps were served with vite preview (production build, not dev server) so behavior matched what a real deployment would do. Used for:

  • Full-page screenshots (page.screenshot({ fullPage: true })) for the before/after visual comparisons.
  • Simulated interaction: clicking buttons/headers via page.evaluate DOM queries (matched by visible text rather than brittle selectors, since neither app’s markup was written with test hooks in mind), page.keyboard.press('Tab'/'Enter'/'Escape') for keyboard-path testing, page.mouse.click(x, y) for backdrop-dismiss testing.
  • Live DOM/ARIA inspection: getAttribute('role'|'aria-*'), tabIndex, getComputedStyle (cursor, border color, computed height), document.activeElement containment checks for the focus-trap tests, input.validity/matches(':invalid') for native constraint validation state.
  • Console/error capture: page.on('console') and page.on('pageerror') listeners kept open through every interaction sequence, not just page load.

Static/source verification — read directly, not summarized from memory. Component source (Select.tsx, DataTable.tsx, FormComponents.tsx, Modal.tsx, etc.), the CLI’s own source (toolcrib/src/lib/*.js) to confirm exactly what init/apply/doctor do and don’t touch, and scripts/build-release.js‘s comments to confirm what’s actually excluded from the shipped release.

Codemod — ts-morph, not regex. The verbatimModuleSyntax fix walked the real TypeScript AST (Project.getSourceFiles(), per-specifier symbol resolution via getSymbol()/getAliasedSymbol()), checking each import specifier’s resolved declaration kind (InterfaceDeclaration/TypeAliasDeclaration only) before marking it type-only — verified directly that this approach can’t misclassify a class/value binding (see the ClassDeclaration check in that section) the way a name-based heuristic could.

Repo/build verification — real toolchain runs, not just tsc --noEmit in isolation. git clone/git diff/git apply --check against fresh clones (not just the working copy that generated a patch) for every patch handed off; npm install (including the repo’s own isolated scripts/ dependency scope, once that mattered); npx tsc --noEmit both with the shipped config and with verbatimModuleSyntax forced on via a scratch config copy; npm run build (the repo’s actual tsc && vite build script, not a substitute); npx vitest run for the full existing test suite (439 tests) rather than cherry-picking the ones expected to pass.

External research — fetched live, not recalled. escape-llc/toolcrib‘s README, CORE.md/REFACTOR_APP.md/component-manifest.json, CHANGES.md, and release list were all pulled via curl/git clone during this session; likewise microsoft/typescript-go‘s README and its own documented feature-status table, and npm view typescript versions/dist-tags to confirm the TS 7 API-surface change directly rather than trust prior training data on a fast-moving toolchain change.